Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wet-days-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'lit-html': patch
---

Make some of our directives generic, so that their DirectiveResult types capture everything needed to infer their render types. This is useful in template type checking.
17 changes: 13 additions & 4 deletions packages/lit-html/src/directives/guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@
*/

import {noChange, Part} from '../lit-html.js';
import {directive, Directive, DirectiveParameters} from '../directive.js';
import {
directive,
Directive,
DirectiveParameters,
DirectiveResult,
} from '../directive.js';

// A sentinel that indicates guard() hasn't rendered anything yet
const initialValue = {};

class GuardDirective extends Directive {
class GuardDirective<T> extends Directive {
private _previousValue: unknown = initialValue;

render(_value: unknown, f: () => unknown) {
render(_value: unknown, f: () => T): T {
return f();
}

Expand All @@ -40,6 +45,10 @@ class GuardDirective extends Directive {
}
}

interface Guard {
<T>(vals: unknown[], f: () => T): DirectiveResult<typeof GuardDirective<T>>;
}

/**
* Prevents re-render of a template function until a single value or an array of
* values changes.
Expand Down Expand Up @@ -81,7 +90,7 @@ class GuardDirective extends Directive {
* @param value the value to check before re-rendering
* @param f the template function
*/
export const guard = directive(GuardDirective);
export const guard: Guard = directive(GuardDirective);

/**
* The type of the class that powers this directive. Necessary for naming the
Expand Down
11 changes: 8 additions & 3 deletions packages/lit-html/src/directives/keyed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import {
Directive,
ChildPart,
DirectiveParameters,
DirectiveResult,
} from '../directive.js';
import {setCommittedValue} from '../directive-helpers.js';

class Keyed extends Directive {
class Keyed<T> extends Directive {
key: unknown = nothing;

render(k: unknown, v: unknown) {
render(k: unknown, v: T): T {
this.key = k;
return v;
}
Expand All @@ -33,6 +34,10 @@ class Keyed extends Directive {
}
}

interface KeyedFunc {
<V>(k: unknown, v: V): DirectiveResult<typeof Keyed<V>>;
}

/**
* Associates a renderable value with a unique key. When the key changes, the
* previous DOM is removed and disposed before rendering the next value, even
Expand All @@ -42,7 +47,7 @@ class Keyed extends Directive {
* with code that expects new data to generate new HTML elements, such as some
* animation techniques.
*/
export const keyed = directive(Keyed);
export const keyed: KeyedFunc = directive(Keyed);

/**
* The type of the class that powers this directive. Necessary for naming the
Expand Down
11 changes: 8 additions & 3 deletions packages/lit-html/src/directives/live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import {
directive,
Directive,
DirectiveParameters,
DirectiveResult,
PartInfo,
PartType,
} from '../directive.js';
import {isSingleExpression, setCommittedValue} from '../directive-helpers.js';

class LiveDirective extends Directive {
class LiveDirective<T> extends Directive {
constructor(partInfo: PartInfo) {
super(partInfo);
if (
Expand All @@ -33,7 +34,7 @@ class LiveDirective extends Directive {
}
}

render(value: unknown) {
render(value: T): T {
return value;
}

Expand Down Expand Up @@ -65,6 +66,10 @@ class LiveDirective extends Directive {
}
}

interface Live {
<T>(value: T): DirectiveResult<typeof LiveDirective<T>>;
}

/**
* Checks binding values against live DOM values, instead of previously bound
* values, when determining whether to update the value.
Expand All @@ -89,7 +94,7 @@ class LiveDirective extends Directive {
* you use `live()` with an attribute binding, make sure that only strings are
* passed in, or the binding will update every render.
*/
export const live = directive(LiveDirective);
export const live: Live = directive(LiveDirective);

/**
* The type of the class that powers this directive. Necessary for naming the
Expand Down
24 changes: 18 additions & 6 deletions packages/lit-html/src/directives/until.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,29 @@

import {Part, noChange} from '../lit-html.js';
import {isPrimitive} from '../directive-helpers.js';
import {directive, AsyncDirective} from '../async-directive.js';
import {
directive,
AsyncDirective,
DirectiveResult,
} from '../async-directive.js';
import {Pauser, PseudoWeakRef} from './private-async-helpers.js';

const isPromise = (x: unknown) => {
const isPromise = (x: unknown): x is Promise<unknown> => {
return !isPrimitive(x) && typeof (x as {then?: unknown}).then === 'function';
};
// Effectively infinity, but a SMI.
const _infinity = 0x3fffffff;

export class UntilDirective extends AsyncDirective {
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

export class UntilDirective<T> extends AsyncDirective {
private __lastRenderedIndex: number = _infinity;
private __values: unknown[] = [];
private __weakThis = new PseudoWeakRef(this);
private __pauser = new Pauser();

render(...args: Array<unknown>): unknown {
return args.find((x) => !isPromise(x)) ?? noChange;
render(...args: Array<T>): UnwrapPromise<T> {
return (args.find((x) => !isPromise(x)) ?? noChange) as UnwrapPromise<T>;
}

override update(_part: Part, args: Array<unknown>) {
Expand Down Expand Up @@ -107,6 +113,12 @@ export class UntilDirective extends AsyncDirective {
}
}

interface Until {
<T extends Array<unknown>>(
...args: T
): DirectiveResult<typeof UntilDirective<T[number]>>;
}

/**
* Renders one of a series of values, including Promises, to a Part.
*
Expand All @@ -128,7 +140,7 @@ export class UntilDirective extends AsyncDirective {
* html`${until(content, html`<span>Loading...</span>`)}`
* ```
*/
export const until = directive(UntilDirective);
export const until: Until = directive(UntilDirective);

/**
* The type of the class that powers this directive. Necessary for naming the
Expand Down
125 changes: 125 additions & 0 deletions packages/lit-html/src/test/directives/types_are_inferrable_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {DirectiveResult} from 'lit-html/directive.js';
import {guard} from 'lit-html/directives/guard.js';
import {classMap} from 'lit-html/directives/class-map.js';
import {keyed} from 'lit-html/directives/keyed.js';
import {live} from 'lit-html/directives/live.js';
import {until} from 'lit-html/directives/until.js';

type GetRenderAs<D extends DirectiveResult> =
D extends DirectiveResult<infer C>
? C extends {new (...args: any[]): {render(...args: any[]): infer RenderAs}}
? RenderAs
: unknown
: unknown;

// We're using ts-expect-error and friends to implement these tests.
/* eslint-disable @typescript-eslint/ban-ts-comment */

// This test is entirely in the type checkeer, it doesn't need to run,
// it passes if it compiles without error.
if (false as boolean) {
// Test the guard directive's type inference
() => {
const v = guard([1, 2, 3], () => 'hi');

type VRendersAs = GetRenderAs<typeof v>;

const vRendersAs = null! as VRendersAs;

vRendersAs satisfies string;
// @ts-expect-error
vRendersAs satisfies number;
};

// Test the classMap directive's type inference
() => {
const v = classMap({
foo: true,
bar: false,
baz: 0,
});

type VRendersAs = GetRenderAs<typeof v>;

const vRendersAs = null! as VRendersAs;

vRendersAs satisfies string;
// @ts-expect-error
vRendersAs satisfies number;
};

// Test the keyed directive's type inference
() => {
const v = keyed('key', 'value');

type VRendersAs = GetRenderAs<typeof v>;

const vRendersAs = null! as VRendersAs;

vRendersAs satisfies string;
// @ts-expect-error
vRendersAs satisfies number;
};

// Test the live directive's type inference
() => {
const v = live('value' as const);

type VRendersAs = GetRenderAs<typeof v>;

const vRendersAs = null! as VRendersAs;

vRendersAs satisfies 'value';
// @ts-expect-error
vRendersAs satisfies number;

const v2 = live(42 as const);
type VRendersAs2 = GetRenderAs<typeof v2>;
const vRendersAs2: VRendersAs2 = null!;
vRendersAs2 satisfies 42;
// @ts-expect-error
vRendersAs2 satisfies string;
};

// Test the until directive's type inference
() => {
const v = until('value' as const, 'loading' as const);

type VRendersAs = GetRenderAs<typeof v>;

const vRendersAs = null! as VRendersAs;

vRendersAs satisfies 'value' | 'loading';
// @ts-expect-error
vRendersAs satisfies number;

const v2 = until(42 as const, 'loading' as const);
type VRendersAs2 = GetRenderAs<typeof v2>;
const vRendersAs2 = null! as VRendersAs2;
vRendersAs2 satisfies 42 | 'loading';
// @ts-expect-error
vRendersAs2 satisfies string;

const v3 = until(Promise.resolve('value' as const), 42 as const);
type VRendersAs3 = GetRenderAs<typeof v3>;
const vRendersAs3 = null! as VRendersAs3;
vRendersAs3 satisfies 'value' | 42;
// @ts-expect-error
vRendersAs3 satisfies string;
// @ts-expect-error
vRendersAs3 satisfies Promise<any>;

const v4 = until(
Promise.resolve(42 as const),
Promise.resolve('something' as const)
);

type VRendersAs4 = GetRenderAs<typeof v4>;
const vRendersAs4 = null! as VRendersAs4;
vRendersAs4 satisfies 42 | 'something';
// @ts-expect-error
vRendersAs4 satisfies string;
// @ts-expect-error
vRendersAs4 satisfies Promise<any>;
};
}
Loading