Skip to content

Commit

Permalink
Add type-tests to verify assignability for template entities
Browse files Browse the repository at this point in the history
These tests ensure `HelperLike`, `ModifierLike` and `ComponentLike`
all have appropriate variance relative to their signatures'
constituent parts.
  • Loading branch information
dfreeman committed Nov 22, 2022
1 parent 76e8423 commit af78188
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 0 deletions.
78 changes: 78 additions & 0 deletions packages/template/__tests__/component-like.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ComponentLike, WithBoundArgs } from '@glint/template';
import { resolve, emitComponent, NamedArgsMarker } from '@glint/template/-private/dsl';
import { expectTypeOf } from 'expect-type';
import { ComponentReturn, NamedArgs } from '../-private/integration';
import TestComponent from './test-component';

{
const NoArgsComponent = {} as ComponentLike<{}>;
Expand Down Expand Up @@ -132,3 +133,80 @@ import { ComponentReturn, NamedArgs } from '../-private/integration';
) => ComponentReturn<{ default: [] }, HTMLCanvasElement>
>();
}

// Assignability
{
// A component with no signaure is a `ComponentLike` with no signature
expectTypeOf(TestComponent<{}>).toMatchTypeOf<ComponentLike>();

// A component whose args are all optional is a `ComponentLike` with no signature
expectTypeOf(TestComponent<{ Args: { optional?: true } }>).toMatchTypeOf<ComponentLike>();

// A component with a required arg can't be used as a blank `ComponentLike`
expectTypeOf(TestComponent<{ Args: { optional: false } }>).not.toMatchTypeOf<ComponentLike>();

// A component that yields a given block can be used without ever passing any blocks
expectTypeOf(TestComponent<{ Blocks: { default: [string] } }>).toMatchTypeOf<ComponentLike>();

// A component that yields specific args can be used as one that cares about fewer of them
expectTypeOf(TestComponent<{ Blocks: { default: [string, number] } }>).toMatchTypeOf<
ComponentLike<{ Blocks: { default: [string, ...unknown[]] } }>
>();

// A component that never yields can't be used as one that accepts a specific block
expectTypeOf(TestComponent).not.toMatchTypeOf<ComponentLike<{ Blocks: { default: [] } }>>();

// `T | null` is useful to humans to signify that a component might splat its ...attributes,
// but from a type perspective it's just the same as `T`
expectTypeOf<ComponentLike<{ Element: HTMLDivElement | null }>>().toEqualTypeOf<
ComponentLike<{ Element: HTMLDivElement }>
>();

// Our canonical internal representation of a no-splattributes component's `Element` is `unknown`
expectTypeOf<ComponentLike>().toEqualTypeOf<ComponentLike<{ Element: unknown }>>();
expectTypeOf<ComponentLike<{ Element: null }>>().toEqualTypeOf<
ComponentLike<{ Element: unknown }>
>();

// A component with all-optional args and any arbitrary element/blocks should be usable
// as a blank `ComponentLike`.
expectTypeOf(
TestComponent<{
Args: { foo?: string };
Element: HTMLImageElement;
Blocks: { default: [] };
}>
).toMatchTypeOf<ComponentLike>();

// Components are contravariant with their named `Args` type
expectTypeOf<ComponentLike<{ Args: { name: string } }>>().toMatchTypeOf<
ComponentLike<{ Args: { name: 'Dan' } }>
>();
expectTypeOf<ComponentLike<{ Args: { name: 'Dan' } }>>().not.toMatchTypeOf<
ComponentLike<{ Args: { name: string } }>
>();

// Components are contravariant with their positional `Args` type
expectTypeOf<ComponentLike<{ Args: { Positional: [name: string] } }>>().toMatchTypeOf<
ComponentLike<{ Args: { Positional: [name: 'Dan'] } }>
>();
expectTypeOf<ComponentLike<{ Args: { Positional: [name: 'Dan'] } }>>().not.toMatchTypeOf<
ComponentLike<{ Args: { Positional: [name: string] } }>
>();

// Components are covariant with their `Element` type
expectTypeOf<ComponentLike<{ Element: HTMLAudioElement }>>().toMatchTypeOf<
ComponentLike<{ Element: HTMLElement }>
>();
expectTypeOf<ComponentLike<{ Element: HTMLElement }>>().not.toMatchTypeOf<
ComponentLike<{ Element: HTMLAudioElement }>
>();

// Components are covariant with their `Blocks`' `Params` types
expectTypeOf(TestComponent<{ Blocks: { default: ['abc', 123] } }>).toMatchTypeOf<
ComponentLike<{ Blocks: { default: [string, number] } }>
>();
expectTypeOf(TestComponent<{ Blocks: { default: [string, number] } }>).not.toMatchTypeOf<
ComponentLike<{ Blocks: { default: ['abc', 123] } }>
>();
}
27 changes: 27 additions & 0 deletions packages/template/__tests__/helper-like.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,30 @@ import { NamedArgs } from '../-private/integration';
(args: NamedArgs<{ age: number; name?: string }>) => string
>();
}

// Assignability
{
// Helpers are contravariant with their named `Args` type
expectTypeOf<HelperLike<{ Args: { Named: { name: string } } }>>().toMatchTypeOf<
HelperLike<{ Args: { Named: { name: 'Dan' } } }>
>();
expectTypeOf<HelperLike<{ Args: { Named: { name: 'Dan' } } }>>().not.toMatchTypeOf<
HelperLike<{ Args: { Named: { name: string } } }>
>();

// Helpers are contravariant with their positional `Args` type
expectTypeOf<HelperLike<{ Args: { Positional: [name: string] } }>>().toMatchTypeOf<
HelperLike<{ Args: { Positional: [name: 'Dan'] } }>
>();
expectTypeOf<HelperLike<{ Args: { Positional: [name: 'Dan'] } }>>().not.toMatchTypeOf<
HelperLike<{ Args: { Positional: [name: string] } }>
>();

// Helpers are contravariant with their `Element` type
expectTypeOf<HelperLike<{ Return: 'Hello, World' }>>().toMatchTypeOf<
HelperLike<{ Return: string }>
>();
expectTypeOf<HelperLike<{ Return: string }>>().not.toMatchTypeOf<
HelperLike<{ Return: 'Hello, World' }>
>();
}
27 changes: 27 additions & 0 deletions packages/template/__tests__/modifier-like.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,30 @@ import { ModifierLike, WithBoundArgs } from '@glint/template';
) => ModifierReturn
>();
}

// Assignability
{
// Modifiers are contravariant with their named `Args` type
expectTypeOf<ModifierLike<{ Args: { Named: { name: string } } }>>().toMatchTypeOf<
ModifierLike<{ Args: { Named: { name: 'Dan' } } }>
>();
expectTypeOf<ModifierLike<{ Args: { Named: { name: 'Dan' } } }>>().not.toMatchTypeOf<
ModifierLike<{ Args: { Named: { name: string } } }>
>();

// Modifiers are contravariant with their positional `Args` type
expectTypeOf<ModifierLike<{ Args: { Positional: [name: string] } }>>().toMatchTypeOf<
ModifierLike<{ Args: { Positional: [name: 'Dan'] } }>
>();
expectTypeOf<ModifierLike<{ Args: { Positional: [name: 'Dan'] } }>>().not.toMatchTypeOf<
ModifierLike<{ Args: { Positional: [name: string] } }>
>();

// Modifiers are contravariant with their `Element` type
expectTypeOf<ModifierLike<{ Element: HTMLElement }>>().toMatchTypeOf<
ModifierLike<{ Element: HTMLAudioElement }>
>();
expectTypeOf<ModifierLike<{ Element: HTMLAudioElement }>>().not.toMatchTypeOf<
ModifierLike<{ Element: HTMLElement }>
>();
}

0 comments on commit af78188

Please sign in to comment.