From 0a89b16b6029e0ae6dea3d159309c9f5db7753ee Mon Sep 17 00:00:00 2001 From: Peter Burns Date: Tue, 12 Aug 2025 17:01:34 -0700 Subject: [PATCH 1/8] Make the Directive types generic, for better type checking of lit templates. This allows a custom directive to carry enough information that the TypeScript type system can tell the type that a DirectiveResult will render. This is backwards compatible, existing directives will generally be inferred to render the type of their `render` functions. Many directives can be updated to themselves be generic, to better express their types, and I've done this for our built in directives. By itself, this doesn't do much, but I've also got a patch to rune's lit analyzer to better type check directives there. --- packages/lit-html/src/async-directive.ts | 2 +- packages/lit-html/src/directive.ts | 8 +- packages/lit-html/src/directives/guard.ts | 12 +- packages/lit-html/src/directives/keyed.ts | 11 +- packages/lit-html/src/directives/live.ts | 11 +- packages/lit-html/src/directives/until.ts | 33 ++++- .../directives/types_are_inferrable_test.ts | 122 ++++++++++++++++++ 7 files changed, 178 insertions(+), 21 deletions(-) create mode 100644 packages/lit-html/src/test/directives/types_are_inferrable_test.ts diff --git a/packages/lit-html/src/async-directive.ts b/packages/lit-html/src/async-directive.ts index c658780fcd..a8379b99b4 100644 --- a/packages/lit-html/src/async-directive.ts +++ b/packages/lit-html/src/async-directive.ts @@ -292,7 +292,7 @@ const installDisconnectAPI = (obj: Disconnectable) => { * render/update to determine whether it is safe to subscribe to resources * that may prevent garbage collection. */ -export abstract class AsyncDirective extends Directive { +export abstract class AsyncDirective extends Directive { // As opposed to other Disconnectables, AsyncDirectives always get notified // when the RootPart connection changes, so the public `isConnected` // is a locally stored variable initialized via its part's getter and synced diff --git a/packages/lit-html/src/directive.ts b/packages/lit-html/src/directive.ts index d171b97ee1..e469bfc21e 100644 --- a/packages/lit-html/src/directive.ts +++ b/packages/lit-html/src/directive.ts @@ -16,8 +16,8 @@ export { PropertyPart, } from './lit-html.js'; -export interface DirectiveClass { - new (part: PartInfo): Directive; +export interface DirectiveClass { + new (part: PartInfo): Directive; } /** @@ -95,7 +95,7 @@ export const directive = * implement `render` and/or `update`, and then pass their subclass to * `directive`. */ -export abstract class Directive implements Disconnectable { +export abstract class Directive implements Disconnectable { //@internal __part!: Part; //@internal @@ -135,7 +135,7 @@ export abstract class Directive implements Disconnectable { return this.update(part, props); } - abstract render(...props: Array): unknown; + abstract render(...props: Array): RenderAs; update(_part: Part, props: Array): unknown { return this.render(...props); diff --git a/packages/lit-html/src/directives/guard.ts b/packages/lit-html/src/directives/guard.ts index c9fbb917b6..959483f287 100644 --- a/packages/lit-html/src/directives/guard.ts +++ b/packages/lit-html/src/directives/guard.ts @@ -5,15 +5,15 @@ */ 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 extends Directive { private _previousValue: unknown = initialValue; - render(_value: unknown, f: () => unknown) { + render(_value: unknown, f: () => T): T { return f(); } @@ -40,6 +40,10 @@ class GuardDirective extends Directive { } } +interface Guard { + (vals: unknown[], f: () => T): DirectiveResult> +} + /** * Prevents re-render of a template function until a single value or an array of * values changes. @@ -81,7 +85,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 diff --git a/packages/lit-html/src/directives/keyed.ts b/packages/lit-html/src/directives/keyed.ts index c65219d29c..950f99fb08 100644 --- a/packages/lit-html/src/directives/keyed.ts +++ b/packages/lit-html/src/directives/keyed.ts @@ -10,13 +10,14 @@ import { Directive, ChildPart, DirectiveParameters, + DirectiveResult, } from '../directive.js'; import {setCommittedValue} from '../directive-helpers.js'; -class Keyed extends Directive { +class Keyed extends Directive { key: unknown = nothing; - render(k: unknown, v: unknown) { + render(k: unknown, v: T): T { this.key = k; return v; } @@ -33,6 +34,10 @@ class Keyed extends Directive { } } +interface KeyedFunc { + (k: unknown, v: V): DirectiveResult>; +} + /** * 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 @@ -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 diff --git a/packages/lit-html/src/directives/live.ts b/packages/lit-html/src/directives/live.ts index 01471488ad..464e920ae5 100644 --- a/packages/lit-html/src/directives/live.ts +++ b/packages/lit-html/src/directives/live.ts @@ -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 extends Directive { constructor(partInfo: PartInfo) { super(partInfo); if ( @@ -33,7 +34,7 @@ class LiveDirective extends Directive { } } - render(value: unknown) { + render(value: T): T { return value; } @@ -65,6 +66,10 @@ class LiveDirective extends Directive { } } +interface Live { + (value: T): DirectiveResult>; +} + /** * Checks binding values against live DOM values, instead of previously bound * values, when determining whether to update the value. @@ -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 diff --git a/packages/lit-html/src/directives/until.ts b/packages/lit-html/src/directives/until.ts index 4a0ab388f9..ac2d3d4a0f 100644 --- a/packages/lit-html/src/directives/until.ts +++ b/packages/lit-html/src/directives/until.ts @@ -6,23 +6,25 @@ 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 => { 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 extends Promise ? U : T; + +export class UntilDirective extends AsyncDirective> { private __lastRenderedIndex: number = _infinity; private __values: unknown[] = []; private __weakThis = new PseudoWeakRef(this); private __pauser = new Pauser(); - render(...args: Array): unknown { - return args.find((x) => !isPromise(x)) ?? noChange; + render(...args: Array): UnwrapPromise { + return (args.find((x) => !isPromise(x)) ?? noChange) as UnwrapPromise; } override update(_part: Part, args: Array) { @@ -107,6 +109,25 @@ export class UntilDirective extends AsyncDirective { } } +interface Until { + (val: T): DirectiveResult>; + (v1: T, v2: U): DirectiveResult>; + (v1: T, v2: U, v3: V): DirectiveResult>; + (v1: T, v2: U, v3: V, v4: W): DirectiveResult>; + (v1: T, v2: U, v3: V, v4: W, v5: X): DirectiveResult>; + (v1: T, v2: U, v3: V, v4: W, v5: X, v6: Y): DirectiveResult>; + ( + v1: T, + v2: U, + v3: V, + v4: W, + v5: X, + v6: Y, + v7: Z + ): DirectiveResult>; + (...args: Array): DirectiveResult>; +} + /** * Renders one of a series of values, including Promises, to a Part. * @@ -128,7 +149,7 @@ export class UntilDirective extends AsyncDirective { * html`${until(content, html`Loading...`)}` * ``` */ -export const until = directive(UntilDirective); +export const until: Until = directive(UntilDirective); /** * The type of the class that powers this directive. Necessary for naming the diff --git a/packages/lit-html/src/test/directives/types_are_inferrable_test.ts b/packages/lit-html/src/test/directives/types_are_inferrable_test.ts new file mode 100644 index 0000000000..ad5552b9d8 --- /dev/null +++ b/packages/lit-html/src/test/directives/types_are_inferrable_test.ts @@ -0,0 +1,122 @@ +import {DirectiveClass, DirectiveResult} from '../../directive.js'; +import {guard} from '../../directives/guard.js'; +import {classMap} from '../../directives/class-map.js'; +import {keyed} from '../../directives/keyed.js'; +import {live} from '../../directives/live.js'; +import {until} from '../../directives/until.js'; + +type GetRenderAs = + D extends DirectiveResult + ? C extends DirectiveClass + ? RenderAs + : unknown + : unknown; + +// 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; + + 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; + + 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; + + 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; + + const vRendersAs = null! as VRendersAs; + + vRendersAs satisfies 'value'; + // @ts-expect-error + vRendersAs satisfies number; + + const v2 = live(42 as const); + type VRendersAs2 = GetRenderAs; + 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; + + 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; + 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; + const vRendersAs3 = null! as VRendersAs3; + vRendersAs3 satisfies 'value' | 42; + // @ts-expect-error + vRendersAs3 satisfies string; + // @ts-expect-error + vRendersAs3 satisfies Promise; + + const v4 = until( + Promise.resolve(42 as const), + Promise.resolve('something' as const) + ); + + type VRendersAs4 = GetRenderAs; + const vRendersAs4 = null! as VRendersAs4; + vRendersAs4 satisfies 42 | 'something'; + // @ts-expect-error + vRendersAs4 satisfies string; + // @ts-expect-error + vRendersAs4 satisfies Promise; + }; +} From e24bab565a35b05d80664a5d1215cfb74f8e6c25 Mon Sep 17 00:00:00 2001 From: Peter Burns Date: Tue, 12 Aug 2025 17:04:44 -0700 Subject: [PATCH 2/8] Changeset --- .changeset/wet-days-rule.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wet-days-rule.md diff --git a/.changeset/wet-days-rule.md b/.changeset/wet-days-rule.md new file mode 100644 index 0000000000..4d3668b889 --- /dev/null +++ b/.changeset/wet-days-rule.md @@ -0,0 +1,5 @@ +--- +"lit-html": patch +--- + +Make the Directive types generic, to allow better type checking of lit templates. From 519dcc606f548129f2264725831975ee6feb87c9 Mon Sep 17 00:00:00 2001 From: Peter Burns Date: Tue, 12 Aug 2025 17:28:45 -0700 Subject: [PATCH 3/8] Lint. --- .../lit-html/src/test/directives/types_are_inferrable_test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/lit-html/src/test/directives/types_are_inferrable_test.ts b/packages/lit-html/src/test/directives/types_are_inferrable_test.ts index ad5552b9d8..be7011e42e 100644 --- a/packages/lit-html/src/test/directives/types_are_inferrable_test.ts +++ b/packages/lit-html/src/test/directives/types_are_inferrable_test.ts @@ -12,6 +12,9 @@ type GetRenderAs = : 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) { From bd9e5e09e7c707668048f6db564cde0850eb245f Mon Sep 17 00:00:00 2001 From: Peter Burns Date: Wed, 13 Aug 2025 21:30:26 +0000 Subject: [PATCH 4/8] Format. --- .changeset/wet-days-rule.md | 2 +- packages/lit-html/src/async-directive.ts | 2 +- packages/lit-html/src/directives/guard.ts | 11 +++++-- packages/lit-html/src/directives/until.ts | 36 +++++++++++++++++++---- 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/.changeset/wet-days-rule.md b/.changeset/wet-days-rule.md index 4d3668b889..6b36d82312 100644 --- a/.changeset/wet-days-rule.md +++ b/.changeset/wet-days-rule.md @@ -1,5 +1,5 @@ --- -"lit-html": patch +'lit-html': patch --- Make the Directive types generic, to allow better type checking of lit templates. diff --git a/packages/lit-html/src/async-directive.ts b/packages/lit-html/src/async-directive.ts index a8379b99b4..5e9bacb3fe 100644 --- a/packages/lit-html/src/async-directive.ts +++ b/packages/lit-html/src/async-directive.ts @@ -292,7 +292,7 @@ const installDisconnectAPI = (obj: Disconnectable) => { * render/update to determine whether it is safe to subscribe to resources * that may prevent garbage collection. */ -export abstract class AsyncDirective extends Directive { +export abstract class AsyncDirective extends Directive { // As opposed to other Disconnectables, AsyncDirectives always get notified // when the RootPart connection changes, so the public `isConnected` // is a locally stored variable initialized via its part's getter and synced diff --git a/packages/lit-html/src/directives/guard.ts b/packages/lit-html/src/directives/guard.ts index 959483f287..82235a385f 100644 --- a/packages/lit-html/src/directives/guard.ts +++ b/packages/lit-html/src/directives/guard.ts @@ -5,7 +5,12 @@ */ import {noChange, Part} from '../lit-html.js'; -import {directive, Directive, DirectiveParameters, DirectiveResult} from '../directive.js'; +import { + directive, + Directive, + DirectiveParameters, + DirectiveResult, +} from '../directive.js'; // A sentinel that indicates guard() hasn't rendered anything yet const initialValue = {}; @@ -40,8 +45,8 @@ class GuardDirective extends Directive { } } -interface Guard { - (vals: unknown[], f: () => T): DirectiveResult> +interface Guard { + (vals: unknown[], f: () => T): DirectiveResult>; } /** diff --git a/packages/lit-html/src/directives/until.ts b/packages/lit-html/src/directives/until.ts index ac2d3d4a0f..929d9f1ec9 100644 --- a/packages/lit-html/src/directives/until.ts +++ b/packages/lit-html/src/directives/until.ts @@ -6,7 +6,11 @@ import {Part, noChange} from '../lit-html.js'; import {isPrimitive} from '../directive-helpers.js'; -import {directive, AsyncDirective, DirectiveResult} from '../async-directive.js'; +import { + directive, + AsyncDirective, + DirectiveResult, +} from '../async-directive.js'; import {Pauser, PseudoWeakRef} from './private-async-helpers.js'; const isPromise = (x: unknown): x is Promise => { @@ -112,10 +116,32 @@ export class UntilDirective extends AsyncDirective> { interface Until { (val: T): DirectiveResult>; (v1: T, v2: U): DirectiveResult>; - (v1: T, v2: U, v3: V): DirectiveResult>; - (v1: T, v2: U, v3: V, v4: W): DirectiveResult>; - (v1: T, v2: U, v3: V, v4: W, v5: X): DirectiveResult>; - (v1: T, v2: U, v3: V, v4: W, v5: X, v6: Y): DirectiveResult>; + ( + v1: T, + v2: U, + v3: V + ): DirectiveResult>; + ( + v1: T, + v2: U, + v3: V, + v4: W + ): DirectiveResult>; + ( + v1: T, + v2: U, + v3: V, + v4: W, + v5: X + ): DirectiveResult>; + ( + v1: T, + v2: U, + v3: V, + v4: W, + v5: X, + v6: Y + ): DirectiveResult>; ( v1: T, v2: U, From 8f389366ed53f8e190fcaf57654c0285091e9414 Mon Sep 17 00:00:00 2001 From: Peter Burns Date: Fri, 15 Aug 2025 03:23:58 +0000 Subject: [PATCH 5/8] Simplify simplify simplify. Turns out, all we need is to make our own generic directives generic enough so that we can infer the return type of their render functions from their DirectiveResult. No need to change directive.ts or async-directive.ts at all. --- packages/lit-html/src/async-directive.ts | 2 +- packages/lit-html/src/directive.ts | 8 ++++---- packages/lit-html/src/directives/guard.ts | 2 +- packages/lit-html/src/directives/keyed.ts | 2 +- packages/lit-html/src/directives/live.ts | 2 +- packages/lit-html/src/directives/until.ts | 2 +- .../src/test/directives/types_are_inferrable_test.ts | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/lit-html/src/async-directive.ts b/packages/lit-html/src/async-directive.ts index 5e9bacb3fe..c658780fcd 100644 --- a/packages/lit-html/src/async-directive.ts +++ b/packages/lit-html/src/async-directive.ts @@ -292,7 +292,7 @@ const installDisconnectAPI = (obj: Disconnectable) => { * render/update to determine whether it is safe to subscribe to resources * that may prevent garbage collection. */ -export abstract class AsyncDirective extends Directive { +export abstract class AsyncDirective extends Directive { // As opposed to other Disconnectables, AsyncDirectives always get notified // when the RootPart connection changes, so the public `isConnected` // is a locally stored variable initialized via its part's getter and synced diff --git a/packages/lit-html/src/directive.ts b/packages/lit-html/src/directive.ts index e469bfc21e..d171b97ee1 100644 --- a/packages/lit-html/src/directive.ts +++ b/packages/lit-html/src/directive.ts @@ -16,8 +16,8 @@ export { PropertyPart, } from './lit-html.js'; -export interface DirectiveClass { - new (part: PartInfo): Directive; +export interface DirectiveClass { + new (part: PartInfo): Directive; } /** @@ -95,7 +95,7 @@ export const directive = * implement `render` and/or `update`, and then pass their subclass to * `directive`. */ -export abstract class Directive implements Disconnectable { +export abstract class Directive implements Disconnectable { //@internal __part!: Part; //@internal @@ -135,7 +135,7 @@ export abstract class Directive implements Disconnectable { return this.update(part, props); } - abstract render(...props: Array): RenderAs; + abstract render(...props: Array): unknown; update(_part: Part, props: Array): unknown { return this.render(...props); diff --git a/packages/lit-html/src/directives/guard.ts b/packages/lit-html/src/directives/guard.ts index 82235a385f..00d6597ed0 100644 --- a/packages/lit-html/src/directives/guard.ts +++ b/packages/lit-html/src/directives/guard.ts @@ -15,7 +15,7 @@ import { // A sentinel that indicates guard() hasn't rendered anything yet const initialValue = {}; -class GuardDirective extends Directive { +class GuardDirective extends Directive { private _previousValue: unknown = initialValue; render(_value: unknown, f: () => T): T { diff --git a/packages/lit-html/src/directives/keyed.ts b/packages/lit-html/src/directives/keyed.ts index 950f99fb08..14683545e4 100644 --- a/packages/lit-html/src/directives/keyed.ts +++ b/packages/lit-html/src/directives/keyed.ts @@ -14,7 +14,7 @@ import { } from '../directive.js'; import {setCommittedValue} from '../directive-helpers.js'; -class Keyed extends Directive { +class Keyed extends Directive { key: unknown = nothing; render(k: unknown, v: T): T { diff --git a/packages/lit-html/src/directives/live.ts b/packages/lit-html/src/directives/live.ts index 464e920ae5..46610905e2 100644 --- a/packages/lit-html/src/directives/live.ts +++ b/packages/lit-html/src/directives/live.ts @@ -15,7 +15,7 @@ import { } from '../directive.js'; import {isSingleExpression, setCommittedValue} from '../directive-helpers.js'; -class LiveDirective extends Directive { +class LiveDirective extends Directive { constructor(partInfo: PartInfo) { super(partInfo); if ( diff --git a/packages/lit-html/src/directives/until.ts b/packages/lit-html/src/directives/until.ts index 929d9f1ec9..8772a6b36d 100644 --- a/packages/lit-html/src/directives/until.ts +++ b/packages/lit-html/src/directives/until.ts @@ -21,7 +21,7 @@ const _infinity = 0x3fffffff; type UnwrapPromise = T extends Promise ? U : T; -export class UntilDirective extends AsyncDirective> { +export class UntilDirective extends AsyncDirective { private __lastRenderedIndex: number = _infinity; private __values: unknown[] = []; private __weakThis = new PseudoWeakRef(this); diff --git a/packages/lit-html/src/test/directives/types_are_inferrable_test.ts b/packages/lit-html/src/test/directives/types_are_inferrable_test.ts index be7011e42e..f0b2041f9c 100644 --- a/packages/lit-html/src/test/directives/types_are_inferrable_test.ts +++ b/packages/lit-html/src/test/directives/types_are_inferrable_test.ts @@ -1,4 +1,4 @@ -import {DirectiveClass, DirectiveResult} from '../../directive.js'; +import {DirectiveResult} from '../../directive.js'; import {guard} from '../../directives/guard.js'; import {classMap} from '../../directives/class-map.js'; import {keyed} from '../../directives/keyed.js'; @@ -7,7 +7,7 @@ import {until} from '../../directives/until.js'; type GetRenderAs = D extends DirectiveResult - ? C extends DirectiveClass + ? C extends {new (...args: any[]): {render(...args: any[]): infer RenderAs}} ? RenderAs : unknown : unknown; From f62c7734c92400be4ae3df4256e2c14dbeec7a4e Mon Sep 17 00:00:00 2001 From: Peter Burns Date: Fri, 15 Aug 2025 03:25:54 +0000 Subject: [PATCH 6/8] Update changeset. --- .changeset/wet-days-rule.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/wet-days-rule.md b/.changeset/wet-days-rule.md index 6b36d82312..6ed110aa3c 100644 --- a/.changeset/wet-days-rule.md +++ b/.changeset/wet-days-rule.md @@ -2,4 +2,4 @@ 'lit-html': patch --- -Make the Directive types generic, to allow better type checking of lit templates. +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. From fa0f6104977711a3460e2733acb9e415ae6eb519 Mon Sep 17 00:00:00 2001 From: Peter Burns Date: Fri, 15 Aug 2025 04:01:55 +0000 Subject: [PATCH 7/8] Fix imports in test. --- .../src/test/directives/types_are_inferrable_test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/lit-html/src/test/directives/types_are_inferrable_test.ts b/packages/lit-html/src/test/directives/types_are_inferrable_test.ts index f0b2041f9c..c0810ad9ef 100644 --- a/packages/lit-html/src/test/directives/types_are_inferrable_test.ts +++ b/packages/lit-html/src/test/directives/types_are_inferrable_test.ts @@ -1,9 +1,9 @@ -import {DirectiveResult} from '../../directive.js'; -import {guard} from '../../directives/guard.js'; -import {classMap} from '../../directives/class-map.js'; -import {keyed} from '../../directives/keyed.js'; -import {live} from '../../directives/live.js'; -import {until} from '../../directives/until.js'; +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 From 8cee2db994c785e1b32e6f2aad94f9c6fa7beebf Mon Sep 17 00:00:00 2001 From: rictic Date: Thu, 21 Aug 2025 12:20:02 -0700 Subject: [PATCH 8/8] More generic until type. --- packages/lit-html/src/directives/until.ts | 41 ++--------------------- 1 file changed, 3 insertions(+), 38 deletions(-) diff --git a/packages/lit-html/src/directives/until.ts b/packages/lit-html/src/directives/until.ts index 8772a6b36d..58c3583658 100644 --- a/packages/lit-html/src/directives/until.ts +++ b/packages/lit-html/src/directives/until.ts @@ -114,44 +114,9 @@ export class UntilDirective extends AsyncDirective { } interface Until { - (val: T): DirectiveResult>; - (v1: T, v2: U): DirectiveResult>; - ( - v1: T, - v2: U, - v3: V - ): DirectiveResult>; - ( - v1: T, - v2: U, - v3: V, - v4: W - ): DirectiveResult>; - ( - v1: T, - v2: U, - v3: V, - v4: W, - v5: X - ): DirectiveResult>; - ( - v1: T, - v2: U, - v3: V, - v4: W, - v5: X, - v6: Y - ): DirectiveResult>; - ( - v1: T, - v2: U, - v3: V, - v4: W, - v5: X, - v6: Y, - v7: Z - ): DirectiveResult>; - (...args: Array): DirectiveResult>; + >( + ...args: T + ): DirectiveResult>; } /**