From 499d266b7cfc1c7a9656765c58f7aa59dcadf64d Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Fri, 1 Apr 2022 10:53:50 -0600 Subject: [PATCH] Fix type distributivity in Glimmer Component Signature When using a `keyof` type to check whether the type parameter for Glimmer Component is a `Signature` or the classic `Args`-only type, if we do not force TS to distribute over union types, it resolves the `keyof` check for union types with no shared members as `never`, and `never extends ` is always true. This in turn meant that for all such unions, as well as for cases where users were providing generic types which could then be further extended in their own subclasses. Accordingly, introduce the standard technique TypeScript provides for opting into distributivity: conditional types are documented to support exactly this. --- .../component/addon/-private/component.ts | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/@glimmer/component/addon/-private/component.ts b/packages/@glimmer/component/addon/-private/component.ts index 154d2edd2..d92fd1686 100644 --- a/packages/@glimmer/component/addon/-private/component.ts +++ b/packages/@glimmer/component/addon/-private/component.ts @@ -54,18 +54,7 @@ type ArgsFor = S extends { Args: infer Args } : { Named: S['Args']; Positional: [] } : { Named: EmptyObject; Positional: [] }; -/** - * Given any allowed shorthand form of a signature, desugars it to its full - * expanded type. - * - * @internal This is only exported so we can avoid duplicating it in - * [Glint](https://github.com/typed-ember/glint) or other such tooling. It is - * *not* intended for public usage, and the specific mechanics it uses may - * change at any time. Although the signature produced by is part of Glimmer's - * public API the existence and mechanics of this specific symbol are *not*, - * so ***DO NOT RELY ON IT***. - */ -export type ExpandSignature = { +type _ExpandSignature = { Element: GetOrElse; Args: keyof T extends 'Args' | 'Element' | 'Blocks' // Is this a `Signature`? ? ArgsFor // Then use `Signature` args @@ -74,11 +63,29 @@ export type ExpandSignature = { ? { [Block in keyof Blocks]: Blocks[Block] extends unknown[] ? { Positional: Blocks[Block] } - : Blocks[Block]; + : Blocks[Block] } : EmptyObject; }; +/** + * Given any allowed shorthand form of a signature, desugars it to its full + * expanded type. + * + * @internal This is only exported so we can avoid duplicating it in + * [Glint](https://github.com/typed-ember/glint) or other such tooling. It is + * *not* intended for public usage, and the specific mechanics it uses may + * change at any time. Although the signature produced by is part of Glimmer's + * public API the existence and mechanics of this specific symbol are *not*, + * so ***DO NOT RELY ON IT***. + */ +// The conditional type here is because TS applies conditional types +// distributively. This means that for union types, checks like `keyof T` get +// all the keys from all elements of the union, instead of ending up as `never` +// and then always falling into the `Signature` path instead of falling back to +// the legacy args handling path. +export type ExpandSignature = T extends any ? _ExpandSignature : never; + /** * @internal we use this type for convenience internally; inference means users * should not normally need to name it @@ -220,7 +227,9 @@ export default class BaseComponent { constructor(owner: unknown, args: Args) { if (DEBUG && !(owner !== null && typeof owner === 'object' && ARGS_SET.has(args))) { throw new Error( - `You must pass both the owner and args to super() in your component: ${this.constructor.name}. You can pass them directly, or use ...arguments to pass all arguments through.` + `You must pass both the owner and args to super() in your component: ${ + this.constructor.name + }. You can pass them directly, or use ...arguments to pass all arguments through.` ); }