Skip to content

Commit

Permalink
Fix issue that affected the original tag() implementation and hence a…
Browse files Browse the repository at this point in the history
…lso the new getByRef() implementation...

Using tag() on a Component that has a lower-case property of instance type `Element` or `Component` in its TemplateSpec
resulted in tag() causing a very strange error due to "circular references". The new getByRef() implementation is based
on how tag() originally worked and saw the same exact issue.

The error was tracked back to IsTerminus<T> returning `false` for any instance type of `Element` or `Component` and the fact that
SpecToTagPaths<T> did not filter out lower-case properties. This resulted in SpecToTagPaths<T> recursively entering a lower-case
`Element`/`Component` property (which is where it should have actually terminated recursion at) and then further recursively enter
the instance's `stage`, `application`, `ctx`, etc properties ultimately resulting in a circular reference.

SpecToTagPaths<T> now only returns paths for upper case ValidRef properties. And IsTerminus<T> returns true for
`Element` and `Component`.

I've also moved many of the internal type functions that existed in Element.d.mts into internalTypes.d.mts and added comprehensive
unit tests for each of them. I've also added a test for the fixed scenario that was originally untested in components-strong-typing.test-d.ts.

This has also been tested with the TypeScript version of TMDB which is how I originally found the issue.
  • Loading branch information
frank-weindel committed Feb 10, 2023
1 parent 69152bb commit dc5a168
Show file tree
Hide file tree
Showing 6 changed files with 596 additions and 213 deletions.
4 changes: 2 additions & 2 deletions src/application/Component.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { HandlerReturnType, HandlerParameters, SignalMapType } from "../internalTypes.mjs";
import Element, { CompileElementTemplateSpecType, InlineElement, ValidRef } from "../tree/Element.mjs";
import { HandlerReturnType, HandlerParameters, SignalMapType, ValidRef } from "../internalTypes.mjs";
import Element, { CompileElementTemplateSpecType, InlineElement } from "../tree/Element.mjs";
import Stage from "../tree/Stage.mjs";
import Application from "./Application.mjs";

Expand Down
200 changes: 199 additions & 1 deletion src/internalTypes.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,202 @@ export type HandlerReturnType<PossibleFunction> =
?
ReturnType<PossibleFunction>
:
void;
void;

/**
* Set of all capital letters
*
* @hidden Internal use only
*/
type Alphabet = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z';

/**
* Any string that begins with a capital letter
*
* @hidden Internal use only
*/
export type ValidRef = `${Alphabet}${string}`;

/**
* Returns `true` if T is a type that should terminate the calculation of
* tag paths.
*
* @hidden Internal use only
*/
type IsTerminus<T> =
T extends (string | number | boolean | any[] | Element.Constructor | Element)
?
true
:
T extends object
?
object extends T
?
true
:
false
:
false

/**
* Generates a union of template spec object path string tuples where the last
* tuple item is the value type for that path (wrapped in a single element tuple)
*
* @privateRemarks
* This is a helper type function for {@link TemplateSpecTags}
*
* Example:
*
* ```ts
* type Result = SpecToTagPaths<{
* MyElement: object
* MyParentElement: {
* MyChildComponent: typeof MyComponent
* MyChildElement: {
* MyGrandChildElement: object
* }
* }
* }>
* ```
*
* Equates to:
*
* ```ts
* type Result =
* ['MyElement', [object]] |
* ['MyParentElement', [{
* MyChildComponent: typeof MyComponent
* MyChildElement: {
* MyGrandChildElement: object
* }
* }]] |
* ['MyParentElement', 'MyChildComponent', [typeof MyComponent]]
* ['MyParentElement', 'MyChildElement', [{ MyGrandChildElement: object }]] |
* ['MyParentElement', 'MyChildElement', 'MyGrandChildElement', [object]];
* ```
*
* @hidden Internal use only
*/
export type SpecToTagPaths<T> =
IsTerminus<T> extends true
?
[[T]]
:
{
[K in Extract<keyof T, ValidRef>]: [K, ...SpecToTagPaths<T[K]>] | [K, [T[K]]]
}[Extract<keyof T, ValidRef>]

/**
* Joins the given path string tuple into a single `.` separated string tag path
*
* @hidden Internal use only
*/
export type Join<T extends string[]> =
T extends [] ? never :
T extends [infer F] ? F :
T extends [infer F, ...infer R] ?
F extends string ?
`${F}.${Join<Extract<R, string[]>>}` : never : string;

/**
* Combines tag paths returned by {@link SpecToTagPaths} into a complete flattened object shape
*
* @privateRemarks
* This is a helper type function for {@link TemplateSpecTags}.
*
* Only path elements that are a valid reference name (i.e. start with a capital letter {@link ValidRef}) are
* included.
*
* Example:
*
* ```ts
* type Result = CombineTagPaths<
* ['MyElement', [object]] |
* ['MyParentElement', [{
* MyChildComponent: typeof Component
* MyChildElement: {
* MyGrandChildElement: object
* }
* }]] |
* ['MyParentElement', 'MyChildComponent', [typeof Component]]
* ['MyParentElement', 'MyChildElement', [{ MyGrandChildElement: object }]] |
* ['MyParentElement', 'MyChildElement', 'MyGrandChildElement', [object]]
* >
* ```
*
* equates to:
*
* ```ts
* type Result = {
* 'MyElement': object;
* 'MyParentElement': {
* MyChildComponent: typeof Component
* MyChildElement: {
* MyGrandChildElement: object
* }
* };
* 'MyParentElement.MyChildComponent': typeof Component;
* 'MyParentElement.MyChildElement': { MyGrandChildElement: object };
* 'MyParentElement.MyChildElement.MyGrandChildElement': object
* }
* ```
*
* @hidden Internal use only
*/
export type CombineTagPaths<TagPaths extends any[]> = {
[PathWithType in TagPaths as PathWithType extends [...infer Path extends string[], [any]] ? Join<Path> : never]:
PathWithType extends [...any, [infer Type]]
?
Type
:
never;
}

/**
* Like {@link CombineTagPaths} but only includes the first level of refs from TagPaths
*
* @privateRemarks
* This is a helper type function for {@link TemplateSpecTags}.
*
* Only path elements that are a valid reference name (i.e. start with a capital letter {@link ValidRef}) are
* included.
*
* Example:
*
* ```ts
* type Result = CombineTagPathsSingleLevel<
* ['MyElement', [object]] |
* ['MyParentElement', [{
* MyChildComponent: typeof Component
* MyChildElement: {
* MyGrandChildElement: object
* }
* }]] |
* ['MyParentElement', 'MyChildComponent', [typeof Component]]
* ['MyParentElement', 'MyChildElement', [{ MyGrandChildElement: object }]] |
* ['MyParentElement', 'MyChildElement', 'MyGrandChildElement', [object]]
* >
* ```
*
* equates to:
*
* ```ts
* type Result = {
* 'MyElement': object;
* 'MyParentElement': {
* MyChildComponent: typeof Component
* MyChildElement: {
* MyGrandChildElement: object
* }
* }
* }
* ```
*/
export type CombineTagPathsSingleLevel<TagPaths extends any[]> = {
[PathWithType in TagPaths as PathWithType extends [infer Key extends string, [any]] ? Key : never]:
PathWithType extends [any, [infer Type]]
?
Type
:
never;
}
Loading

0 comments on commit dc5a168

Please sign in to comment.