Skip to content

Commit

Permalink
TypeScript: Fix generic paramater Ref value getByRef() bug
Browse files Browse the repository at this point in the history
  • Loading branch information
frank-weindel committed Jan 23, 2023
1 parent 7006194 commit 69152bb
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 19 deletions.
84 changes: 73 additions & 11 deletions src/tree/Element.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -135,16 +135,16 @@ export type TransformPossibleElement<Key, PossibleElementConstructor, Default =
export type IsLooseTemplateSpec<TemplateSpec extends Element.TemplateSpec> = string extends keyof TemplateSpec ? true : false;

/**
* Gets an object shape containing all the Refs (child Element / Components) in a TemplateSpec
* Remove index signatures from an object
*
* @privateRemarks
* The refs are transformed into proper Element / Component references
* Converts a loose Element template to a strong Element template by
* removing the index signature that Element.TemplateSpecLoose adds
*
* @hidden Internal use only
*/
export type TemplateSpecRefs<TemplateSpec extends Element.TemplateSpec> = {
[P in keyof TemplateSpec as TransformPossibleElement<P, TemplateSpec[P], never> extends never ? never : P]:
TransformPossibleElement<P, TemplateSpec[P], never>
export type RemoveIndex<T> = {
[ K in keyof T as string extends K ? never : number extends K ? never : K ] : T[K]
};

/**
Expand Down Expand Up @@ -173,7 +173,7 @@ type IsTerminus<T> =
* tuple item is the value type for that path (wrapped in a single element tuple)
*
* @privateRemarks
* This is a helper type function for {@link TaggedElements}
* This is a helper type function for {@link TemplateSpecTags}
*
* Example:
*
Expand Down Expand Up @@ -232,7 +232,7 @@ type Join<T extends string[]> =
* Combines tag paths returned by {@link SpecToTagPaths} into a complete flattened object shape
*
* @privateRemarks
* This is a helper type function for {@link TaggedElements}.
* 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.
Expand Down Expand Up @@ -282,14 +282,63 @@ type CombineTagPaths<TagPaths extends any[]> = {
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 = CombineTagPaths<
* ['MyElement', [object]] |
* ['MyParentElement', [{
* MyChildComponent: typeof MyComponent
* MyChildElement: {
* MyGrandChildElement: object
* }
* }]] |
* ['MyParentElement', 'MyChildComponent', [typeof MyComponent]]
* ['MyParentElement', 'MyChildElement', [{ MyGrandChildElement: object }]] |
* ['MyParentElement', 'MyChildElement', 'MyGrandChildElement', [object]]
* >
* ```
*
* equates to:
*
* ```ts
* type Result = {
* 'MyElement': object;
* 'MyParentElement': {
* MyChildComponent: typeof MyComponent
* MyChildElement: {
* MyGrandChildElement: object
* }
* };
* }
* ```
*/
type CombineTagPathsSingleLevel<TagPaths extends any[]> = {
[PathWithType in TagPaths as PathWithType extends [infer Key extends ValidRef, [any]] ? Key : never]:
PathWithType extends [any, [infer Type]]
?
Type
:
never;
}

/**
* Returns a flattened map of the TemplateSpec where each key is is a `.` separated tag path to an element
*
* @privateRemarks
*
* Example:
* ```ts
* type Result = TaggedElements<{
* type Result = TemplateSpecTags<{
* MyElement: object
* MyParentElement: {
* MyChildComponent: typeof MyComponent
Expand Down Expand Up @@ -319,10 +368,22 @@ type CombineTagPaths<TagPaths extends any[]> = {
*
* @hidden Internal use only
*/
export type TaggedElements<TemplateSpec extends Element.TemplateSpec> = {
export type TemplateSpecTags<TemplateSpec extends Element.TemplateSpec> = {
[K in keyof CombineTagPaths<SpecToTagPaths<TemplateSpec>>]: TransformPossibleElement<K, CombineTagPaths<SpecToTagPaths<TemplateSpec>>[K]>;
};

/**
* Gets an object shape containing all the Refs (child Element / Components) in a TemplateSpec
*
* @privateRemarks
* The refs are transformed into proper Element / Component references
*
* @hidden Internal use only
*/
export type TemplateSpecRefs<TemplateSpec extends Element.TemplateSpec> = {
[K in keyof CombineTagPathsSingleLevel<SpecToTagPaths<TemplateSpec>>]: TransformPossibleElement<K, CombineTagPathsSingleLevel<SpecToTagPaths<TemplateSpec>>[K]>;
};

//
// Public types
//
Expand Down Expand Up @@ -1583,7 +1644,7 @@ declare class Element<
* information.
* @param tagName `.` separated tag path
*/
tag<Path extends keyof TaggedElements<TemplateSpecType>>(tagName: Path): TaggedElements<TemplateSpecType>[Path] | undefined;
tag<Path extends keyof TemplateSpecTags<RemoveIndex<TemplateSpecType>>>(tagName: Path): TemplateSpecTags<RemoveIndex<TemplateSpecType>>[Path] | undefined;
tag(tagName: IsLooseTemplateSpec<TemplateSpecType> extends true ? string : never): any;
/**
* Returns all Elements from the subtree that have this tag.
Expand All @@ -1606,7 +1667,8 @@ declare class Element<
*
* @param ref
*/
getByRef<RefKey extends keyof TemplateSpecRefs<TemplateSpecType>>(ref: RefKey): TemplateSpecRefs<TemplateSpecType>[RefKey] | undefined;
getByRef<RefKey extends keyof TemplateSpecRefs<RemoveIndex<TemplateSpecType>>>(ref: RefKey): TemplateSpecRefs<RemoveIndex<TemplateSpecType>>[RefKey] | undefined;
getByRef(tagName: IsLooseTemplateSpec<TemplateSpecType> extends true ? string : never): any;

/**
* Get the location identifier of this Element
Expand Down
111 changes: 111 additions & 0 deletions test-d/Components/component-generic-ref-type-subclassing.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { expectType } from 'tsd';
import lng from '../../index.js';
import { TemplateSpecTags, TemplateSpecRefs } from '../../src/tree/Element.mjs';

///
/// Test that we can create a Component class that accepts a generic Component type
/// for a Ref in its template spec
///

class Header extends lng.Component {
headerSpecificProperty = 'abc'
}

export interface IPageTemplateSpec<
T extends lng.Component.Constructor = lng.Component.Constructor,
> extends lng.Component.TemplateSpec {
Header: typeof Header
Content: T
}

export interface IPageTypeConfig extends lng.Component.TypeConfig {
IsPage: true
}

export class Page<T extends lng.Component.Constructor = lng.Component.Constructor>
extends lng.Component<IPageTemplateSpec<T>, IPageTypeConfig>
implements lng.Component.ImplementTemplateSpec<IPageTemplateSpec<T>>
{
static override _template(): lng.Component.Template<IPageTemplateSpec> {
return {
w: (w: number) => w,
h: (h: number) => h,
rect: true,
color: 0xff0e0e0e,

Header: {
type: Header,
},
Content: undefined,
}
}

override _init() {
/// Content (Direct checks)
expectType<NonNullable<TemplateSpecTags<IPageTemplateSpec<T>>["Content"]>>(this.Content_tag);
expectType<NonNullable<TemplateSpecRefs<IPageTemplateSpec<T>>["Content"]>>(this.Content_getByRef);
expectType<TemplateSpecTags<IPageTemplateSpec<T>>["Content"] | undefined>(this.tag('Content'));
expectType<TemplateSpecRefs<IPageTemplateSpec<T>>["Content"] | undefined>(this.getByRef('Content'));

/// Content (Upcast checks)
expectType<lng.Component>((this as Page).Content_tag);
expectType<lng.Component>((this as Page).Content_getByRef);
expectType<lng.Component | undefined>((this as Page).tag('Content'));
expectType<lng.Component | undefined>((this as Page).getByRef('Content'));

/// Header (direct checks)
expectType<NonNullable<TemplateSpecTags<IPageTemplateSpec<T>>["Header"]>>(this.Header_tag);
expectType<NonNullable<TemplateSpecRefs<IPageTemplateSpec<T>>["Header"]>>(this.Header_getByRef);
expectType<TemplateSpecTags<IPageTemplateSpec<T>>["Header"] | undefined>(this.tag('Header'));
expectType<TemplateSpecRefs<IPageTemplateSpec<T>>["Header"] | undefined>(this.getByRef('Header'));

/// Header (upcast checks)
expectType<Header>((this as Page).Header_tag);
expectType<Header>((this as Page).Header_getByRef);
expectType<Header | undefined>((this as Page).tag('Header'));
expectType<Header | undefined>((this as Page).getByRef('Header'));
}

protected Content_tag = this.tag('Content')!
protected Content_getByRef = this.getByRef('Content')!;

protected Header_tag = this.tag('Header')!
protected Header_getByRef = this.getByRef('Header')!;
}


class List extends lng.Component {
listSpecificProperty = true
}

export class Discovery extends Page<typeof List> {
static override _template(): lng.Component.Template<IPageTemplateSpec<typeof List>> {
// Must assert the specific template type to the type of the template spec
// because `super._template()` isn't/can't be aware of List
const pageTemplate = super._template() as lng.Component.Template<
IPageTemplateSpec<typeof List>
>

pageTemplate.Content = {
type: List,
}

return pageTemplate
}

override _init() {
/// Content (in sub-class)
expectType<List>(this.Content_tag);
expectType<List>(this.Content_getByRef);
expectType<List | undefined>(this.tag('Content'));
expectType<List | undefined>(this.getByRef('Content'));
expectType<boolean>(this.Content_tag.listSpecificProperty);
expectType<boolean>(this.Content_getByRef.listSpecificProperty);

/// Header (in sub-class)
expectType<Header>(this.Header_tag);
expectType<Header>(this.Header_getByRef);
expectType<Header | undefined>(this.tag('Header'));
expectType<Header | undefined>(this.getByRef('Header'));
}
}
1 change: 0 additions & 1 deletion test-d/Components/components-loose.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
* Tests for loosely typed Components with TemplateSpecLoose
*/
import lng from '../../index.js';
import { TaggedElements } from '../../src/tree/Element.mjs';

// Should be able to create a loose Component with unknown properties
class MyLooseComponentA extends lng.Component {
Expand Down
2 changes: 1 addition & 1 deletion test-d/Components/components-strong-typing.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

import { expectType } from 'tsd';
import { Lightning } from '../../index.typedoc.js';
import { TaggedElements, SpecToTagPaths } from '../../src/tree/Element.mjs';
import { TemplateSpecTags, SpecToTagPaths } from '../../src/tree/Element.mjs';
import { MyLooseComponentC } from './components-loose.test-d.js';

namespace MyComponentA {
Expand Down
10 changes: 5 additions & 5 deletions test-d/Elements/Element-Types.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { expectAssignable, expectType } from 'tsd';
import lng from '../../index.js';
import { CompileComponentTemplateSpecType } from '../../src/application/Component.mjs';
import { SignalMapType } from '../../src/internalTypes.mjs';
import { InlineElement, IsLooseTemplateSpec, SmoothTemplate, TaggedElements, TemplateSpecRefs, TransformPossibleElement, TransitionsTemplate } from '../../src/tree/Element.mjs';
import { InlineElement, IsLooseTemplateSpec, SmoothTemplate, TemplateSpecTags, TemplateSpecRefs, TransformPossibleElement, TransitionsTemplate } from '../../src/tree/Element.mjs';

export interface TestTemplateSpec extends lng.Component.TemplateSpec {
prop1: string;
Expand Down Expand Up @@ -302,11 +302,11 @@ function TemplateSpecRefs_Test() {
}

//
// TaggedElements
// TemplateSpecTags
//
function TaggedElements_Test() {
function TemplateSpecTags_Test() {
/// Strong template specs returns flat tag path map
type T1000 = TaggedElements<TestTemplateSpec>;
type T1000 = TemplateSpecTags<TestTemplateSpec>;
expectType<{
'MyStrongElement_InlineEmpty': lng.Element<InlineElement<{}>>;
'MyStrongElement_InlineEmpty2': lng.Element<InlineElement<object>>;
Expand All @@ -323,7 +323,7 @@ function TaggedElements_Test() {
}>({} as T1000);

/// Loose template specs return empty object type
type T2000 = TaggedElements<TestTemplateSpec & lng.Component.TemplateSpecLoose>;
type T2000 = TemplateSpecTags<TestTemplateSpec & lng.Component.TemplateSpecLoose>;
expectType<{}>({} as T2000);
}

Expand Down
3 changes: 2 additions & 1 deletion tsconfig.typedoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@
"src/tree/Element.d.mts:TransitionsTemplate",
"src/internalTypes.d.mts:HandlerReturnType",
"src/tree/Element.d.mts:TemplateSpecRefs",
"src/tree/Element.d.mts:TaggedElements",
"src/tree/Element.d.mts:TemplateSpecTags",
"src/animation/AnimationSettings.d.mts:AnimationForceLiteral",
"src/animation/AnimationSettings.d.mts:AnimationForceType",
"src/internalTypes.d.mts:HandlerParameters",
"src/internalTypes.d.mts:EventMapType",
"src/tree/Element.d.mts:IsLooseTemplateSpec",
"src/tree/Element.d.mts:RemoveIndex"
]
}
}

0 comments on commit 69152bb

Please sign in to comment.