From 3e327f776428be128d604a0ff0ae3a88835fc2b5 Mon Sep 17 00:00:00 2001 From: benzaria Date: Mon, 2 Jun 2025 19:53:53 +0100 Subject: [PATCH 01/17] Add `LiteralList` and `JoinUnion` types **Added**: - `LiteralList` a validator to check if a Tuple have exactly one of each member in given Union, No duplicate, Extras, or Less elements. - `JoinUnion` join a union members with the given delimiter (default: ','). **Modified** - `Join` delimiter (default: ','). Like `Array.join` default behavior. --- source/join-union.d.ts | 7 ++++ source/join.d.ts | 2 +- source/literal-list.d.ts | 34 +++++++++++++++++++ test-d/join.ts | 6 ++-- test-d/literal-list.ts | 72 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 source/join-union.d.ts create mode 100644 source/literal-list.d.ts create mode 100644 test-d/literal-list.ts diff --git a/source/join-union.d.ts b/source/join-union.d.ts new file mode 100644 index 000000000..83b3f1dcf --- /dev/null +++ b/source/join-union.d.ts @@ -0,0 +1,7 @@ +import type {UnionToTuple} from './union-to-tuple.d.ts'; +import type {Join, JoinableItem} from './join.d.ts'; + +type JoinUnion = + UnionToTuple extends infer Tuple extends JoinableItem[] + ? Join + : ''; diff --git a/source/join.d.ts b/source/join.d.ts index 442f85d37..29c157b32 100644 --- a/source/join.d.ts +++ b/source/join.d.ts @@ -50,7 +50,7 @@ const path: Join<['hello' | undefined, 'world' | null], '.'> = ['hello', 'world' */ export type Join< Items extends readonly JoinableItem[], - Delimiter extends string, + Delimiter extends string = ',', > = Items extends readonly [] ? '' : Items extends readonly [JoinableItem?] diff --git a/source/literal-list.d.ts b/source/literal-list.d.ts new file mode 100644 index 000000000..1abbeb251 --- /dev/null +++ b/source/literal-list.d.ts @@ -0,0 +1,34 @@ +import type {UnionToTuple} from './union-to-tuple.d.ts'; +import type {BuildTuple} from './internal/tuple.d.ts'; +import type {Join, JoinableItem} from './join.d.ts'; +import type {JoinUnion} from './join-union.d.ts'; +import type {IsNever} from './is-never.d.ts'; + +type TupleOfUnions = UnionToTuple['length'] extends infer Length extends number + ? Readonly> + : never; + +type TupleAsString = `['${ + [T] extends [JoinableItem[]] + ? Join + : JoinUnion +}']`; + +type LiteralList< + T extends readonly any[], + U extends readonly any[] | any, +> = ( + ([U] extends [readonly any[]] ? U : TupleOfUnions) extends infer V extends readonly any[] + ? V['length'] extends T['length'] + ? Exclude extends infer TnV + ? Exclude extends infer VnT + ? IsNever extends true + ? IsNever extends true + ? T + : never | `Type ${TupleAsString} is missing Properties: ${TupleAsString}` + : never | `Type ${TupleAsString} has extra Properties: ${TupleAsString}` + : never + : never + : never | `Type ${TupleAsString} is not the required Length of: ${V['length']}` + : never +); diff --git a/test-d/join.ts b/test-d/join.ts index 6ce652480..afa9c2684 100644 --- a/test-d/join.ts +++ b/test-d/join.ts @@ -45,17 +45,17 @@ expectNotAssignable<'test.'>(singleTupleJoined); // Typeof of const tuple. const tuple = ['foo', 'bar', 'baz'] as const; -const joinedTuple: Join = 'foo,bar,baz'; +const joinedTuple: Join = 'foo,bar,baz'; expectType<'foo,bar,baz'>(joinedTuple); // Typeof of const empty tuple. const emptyTuple = [] as const; -const joinedEmptyTuple: Join = ''; +const joinedEmptyTuple: Join = ''; expectType<''>(joinedEmptyTuple); // Typeof of string[]. const stringArray = ['foo', 'bar', 'baz']; -const joinedStringArray: Join = ''; +const joinedStringArray: Join = ''; expectType(joinedStringArray); expectNotAssignable<'foo,bar,baz'>(joinedStringArray); diff --git a/test-d/literal-list.ts b/test-d/literal-list.ts new file mode 100644 index 000000000..ee4167faa --- /dev/null +++ b/test-d/literal-list.ts @@ -0,0 +1,72 @@ +import {expectType} from 'tsd'; +import type {LiteralList} from '../source/literal-list.d.ts'; + +type Union = 'a' | 'b' | 'c' | 'd'; + +declare const bar1: ['a', 'b', 'c', 'd']; +declare const bar2: ['a', 'b', 'd', 'c']; +declare const bar3: ['a', 'c', 'b', 'd']; +declare const bar4: ['a', 'c', 'd', 'b']; +declare const bar5: ['a', 'd', 'b', 'c']; +declare const bar6: ['a', 'd', 'c', 'b']; +declare const bar7: ['b', 'a', 'c', 'd']; +declare const bar8: ['b', 'a', 'd', 'c']; +declare const bar9: ['b', 'c', 'a', 'd']; +declare const bar10: ['b', 'c', 'd', 'a']; +declare const bar11: ['b', 'd', 'a', 'c']; +declare const bar12: ['b', 'd', 'c', 'a']; +declare const bar13: ['c', 'a', 'b', 'd']; +declare const bar14: ['c', 'a', 'd', 'b']; +declare const bar15: ['c', 'b', 'a', 'd']; +declare const bar16: ['c', 'b', 'd', 'a']; +declare const bar17: ['c', 'd', 'a', 'b']; +declare const bar18: ['c', 'd', 'b', 'a']; +declare const bar19: ['d', 'a', 'b', 'c']; +declare const bar20: ['d', 'a', 'c', 'b']; +declare const bar21: ['d', 'b', 'a', 'c']; +declare const bar22: ['d', 'b', 'c', 'a']; +declare const bar23: ['d', 'c', 'a', 'b']; +declare const bar24: ['d', 'c', 'b', 'a']; + +expectType(bar1 satisfies LiteralList); +expectType(bar2 satisfies LiteralList); +expectType(bar3 satisfies LiteralList); +expectType(bar4 satisfies LiteralList); +expectType(bar5 satisfies LiteralList); +expectType(bar6 satisfies LiteralList); +expectType(bar7 satisfies LiteralList); +expectType(bar8 satisfies LiteralList); +expectType(bar9 satisfies LiteralList); +expectType(bar10 satisfies LiteralList); +expectType(bar11 satisfies LiteralList); +expectType(bar12 satisfies LiteralList); +expectType(bar13 satisfies LiteralList); +expectType(bar14 satisfies LiteralList); +expectType(bar15 satisfies LiteralList); +expectType(bar16 satisfies LiteralList); +expectType(bar17 satisfies LiteralList); +expectType(bar18 satisfies LiteralList); +expectType(bar19 satisfies LiteralList); +expectType(bar20 satisfies LiteralList); +expectType(bar21 satisfies LiteralList); +expectType(bar22 satisfies LiteralList); +expectType(bar23 satisfies LiteralList); +expectType(bar24 satisfies LiteralList); + +declare const foo1: ['a', 'b', 'c']; +declare const foo2: ['b', 'c', 'd']; +declare const foo3: ['c', 'a', 'd', 'b', 'f']; +declare const foo4: ['c', 'd', 'e', 'b', 'a']; +declare const foo5: ['a', 'd', 'b', 'b']; +declare const foo6: ['a', 'a', 'a', 'b']; +declare const foo7: ['b', 'a', 'c', 'm']; +declare const foo8: ['b', 'e', 'c', 'd']; + +expectType<'Type [\'a\', \'b\', \'c\'] is not the required Length of: 4'>({} as LiteralList); +expectType<'Type [\'b\', \'c\', \'d\'] is not the required Length of: 4'>({} as LiteralList); +expectType<'Type [\'c\', \'a\', \'d\', \'b\', \'f\'] is not the required Length of: 4'>({} as LiteralList); +expectType<'Type [\'c\', \'d\', \'e\', \'b\', \'a\'] is not the required Length of: 4'>({} as LiteralList); +expectType<'Type [\'a\', \'d\', \'b\', \'b\'] is missing Properties: [\'c\']'>({} as LiteralList); +expectType<'Type [\'a\', \'a\', \'a\', \'b\'] is missing Properties: [\'c\', \'d\']'>({} as LiteralList); +expectType<'Type [\'b\', \'a\', \'c\', \'m\'] has extra Properties: [\'m\']'>({} as LiteralList); +expectType<'Type [\'b\', \'a\', \'c\', \'m\'] has extra Properties: [\'m\']'>({} as LiteralList); From cdc113c81ec03c076fd24dc5c2a4897c9f4a2a67 Mon Sep 17 00:00:00 2001 From: benzaria Date: Mon, 2 Jun 2025 20:16:00 +0100 Subject: [PATCH 02/17] Fix: test errors --- source/join.d.ts | 2 +- test-d/literal-list.ts | 59 +++++++++++++++++++++--------------------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/source/join.d.ts b/source/join.d.ts index 29c157b32..66acdb71c 100644 --- a/source/join.d.ts +++ b/source/join.d.ts @@ -9,7 +9,7 @@ type NullishCoalesce< > = Value extends undefined | null ? NonNullable | Fallback : Value; /** -Join an array of strings and/or numbers using the given string as a delimiter. +Join an array of strings and/or numbers using the given string as a delimiter (default: `,`). Use-case: Defining key paths in a nested object. For example, for dot-notation fields in MongoDB queries. diff --git a/test-d/literal-list.ts b/test-d/literal-list.ts index ee4167faa..96062e1d0 100644 --- a/test-d/literal-list.ts +++ b/test-d/literal-list.ts @@ -1,7 +1,8 @@ import {expectType} from 'tsd'; -import type {LiteralList} from '../source/literal-list.d.ts'; +import type {LiteralList, TupleOfUnions} from '../source/literal-list.d.ts'; type Union = 'a' | 'b' | 'c' | 'd'; +type UnionList = TupleOfUnions; declare const bar1: ['a', 'b', 'c', 'd']; declare const bar2: ['a', 'b', 'd', 'c']; @@ -28,45 +29,45 @@ declare const bar22: ['d', 'b', 'c', 'a']; declare const bar23: ['d', 'c', 'a', 'b']; declare const bar24: ['d', 'c', 'b', 'a']; -expectType(bar1 satisfies LiteralList); -expectType(bar2 satisfies LiteralList); -expectType(bar3 satisfies LiteralList); -expectType(bar4 satisfies LiteralList); -expectType(bar5 satisfies LiteralList); -expectType(bar6 satisfies LiteralList); -expectType(bar7 satisfies LiteralList); -expectType(bar8 satisfies LiteralList); -expectType(bar9 satisfies LiteralList); -expectType(bar10 satisfies LiteralList); -expectType(bar11 satisfies LiteralList); -expectType(bar12 satisfies LiteralList); -expectType(bar13 satisfies LiteralList); -expectType(bar14 satisfies LiteralList); -expectType(bar15 satisfies LiteralList); -expectType(bar16 satisfies LiteralList); -expectType(bar17 satisfies LiteralList); -expectType(bar18 satisfies LiteralList); -expectType(bar19 satisfies LiteralList); -expectType(bar20 satisfies LiteralList); -expectType(bar21 satisfies LiteralList); -expectType(bar22 satisfies LiteralList); -expectType(bar23 satisfies LiteralList); -expectType(bar24 satisfies LiteralList); +expectType(bar1 satisfies LiteralList); +expectType(bar2 satisfies LiteralList); +expectType(bar3 satisfies LiteralList); +expectType(bar4 satisfies LiteralList); +expectType(bar5 satisfies LiteralList); +expectType(bar6 satisfies LiteralList); +expectType(bar7 satisfies LiteralList); +expectType(bar8 satisfies LiteralList); +expectType(bar9 satisfies LiteralList); +expectType(bar10 satisfies LiteralList); +expectType(bar11 satisfies LiteralList); +expectType(bar12 satisfies LiteralList); +expectType(bar13 satisfies LiteralList); +expectType(bar14 satisfies LiteralList); +expectType(bar15 satisfies LiteralList); +expectType(bar16 satisfies LiteralList); +expectType(bar17 satisfies LiteralList); +expectType(bar18 satisfies LiteralList); +expectType(bar19 satisfies LiteralList); +expectType(bar20 satisfies LiteralList); +expectType(bar21 satisfies LiteralList); +expectType(bar22 satisfies LiteralList); +expectType(bar23 satisfies LiteralList); +expectType(bar24 satisfies LiteralList); declare const foo1: ['a', 'b', 'c']; declare const foo2: ['b', 'c', 'd']; declare const foo3: ['c', 'a', 'd', 'b', 'f']; declare const foo4: ['c', 'd', 'e', 'b', 'a']; declare const foo5: ['a', 'd', 'b', 'b']; -declare const foo6: ['a', 'a', 'a', 'b']; +declare const foo6: ['a', 'a', 'c', 'b']; declare const foo7: ['b', 'a', 'c', 'm']; -declare const foo8: ['b', 'e', 'c', 'd']; +declare const foo8: ['b', 'c', 'e', 'd']; expectType<'Type [\'a\', \'b\', \'c\'] is not the required Length of: 4'>({} as LiteralList); expectType<'Type [\'b\', \'c\', \'d\'] is not the required Length of: 4'>({} as LiteralList); expectType<'Type [\'c\', \'a\', \'d\', \'b\', \'f\'] is not the required Length of: 4'>({} as LiteralList); expectType<'Type [\'c\', \'d\', \'e\', \'b\', \'a\'] is not the required Length of: 4'>({} as LiteralList); expectType<'Type [\'a\', \'d\', \'b\', \'b\'] is missing Properties: [\'c\']'>({} as LiteralList); -expectType<'Type [\'a\', \'a\', \'a\', \'b\'] is missing Properties: [\'c\', \'d\']'>({} as LiteralList); -expectType<'Type [\'b\', \'a\', \'c\', \'m\'] has extra Properties: [\'m\']'>({} as LiteralList); +expectType<'Type [\'a\', \'a\', \'c\', \'b\'] is missing Properties: [\'d\']'>({} as LiteralList); expectType<'Type [\'b\', \'a\', \'c\', \'m\'] has extra Properties: [\'m\']'>({} as LiteralList); +expectType<'Type [\'b\', \'c\', \'e\', \'d\'] has extra Properties: [\'e\']'>({} as LiteralList); From 452e4fcd1bc5dd30c997a7add7f578cd13da891f Mon Sep 17 00:00:00 2001 From: benzaria Date: Fri, 6 Jun 2025 21:53:08 +0100 Subject: [PATCH 03/17] reverte changes on `Join` --- source/join.d.ts | 4 ++-- test-d/join.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/source/join.d.ts b/source/join.d.ts index 66acdb71c..442f85d37 100644 --- a/source/join.d.ts +++ b/source/join.d.ts @@ -9,7 +9,7 @@ type NullishCoalesce< > = Value extends undefined | null ? NonNullable | Fallback : Value; /** -Join an array of strings and/or numbers using the given string as a delimiter (default: `,`). +Join an array of strings and/or numbers using the given string as a delimiter. Use-case: Defining key paths in a nested object. For example, for dot-notation fields in MongoDB queries. @@ -50,7 +50,7 @@ const path: Join<['hello' | undefined, 'world' | null], '.'> = ['hello', 'world' */ export type Join< Items extends readonly JoinableItem[], - Delimiter extends string = ',', + Delimiter extends string, > = Items extends readonly [] ? '' : Items extends readonly [JoinableItem?] diff --git a/test-d/join.ts b/test-d/join.ts index afa9c2684..6ce652480 100644 --- a/test-d/join.ts +++ b/test-d/join.ts @@ -45,17 +45,17 @@ expectNotAssignable<'test.'>(singleTupleJoined); // Typeof of const tuple. const tuple = ['foo', 'bar', 'baz'] as const; -const joinedTuple: Join = 'foo,bar,baz'; +const joinedTuple: Join = 'foo,bar,baz'; expectType<'foo,bar,baz'>(joinedTuple); // Typeof of const empty tuple. const emptyTuple = [] as const; -const joinedEmptyTuple: Join = ''; +const joinedEmptyTuple: Join = ''; expectType<''>(joinedEmptyTuple); // Typeof of string[]. const stringArray = ['foo', 'bar', 'baz']; -const joinedStringArray: Join = ''; +const joinedStringArray: Join = ''; expectType(joinedStringArray); expectNotAssignable<'foo,bar,baz'>(joinedStringArray); From d90091b583c9936f92bb5b882f412538b44e8191 Mon Sep 17 00:00:00 2001 From: benzaria Date: Sat, 7 Jun 2025 02:33:20 +0100 Subject: [PATCH 04/17] Add: `JoinUnion` tests, docs --- source/join-union.d.ts | 34 ++++++++++++++++++++++++++++++---- test-d/join-union.ts | 23 +++++++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 test-d/join-union.ts diff --git a/source/join-union.d.ts b/source/join-union.d.ts index 83b3f1dcf..7a5b7d381 100644 --- a/source/join-union.d.ts +++ b/source/join-union.d.ts @@ -1,7 +1,33 @@ import type {UnionToTuple} from './union-to-tuple.d.ts'; import type {Join, JoinableItem} from './join.d.ts'; -type JoinUnion = - UnionToTuple extends infer Tuple extends JoinableItem[] - ? Join - : ''; +/** +Join an union of {@link JoinableItem `JoinableItems`} using the given string as a delimiter (default: `,`). + +@example +``` +import type {JoinUnion} from 'type-fest'; + +type T1 = JoinUnion<'a' | 'b' | 'c'>; +// => "a, b, c" + +type T2 = JoinUnion<1 | 2 | 3, ' | '>; +// => "1 | 2 | 3" + +type T3 = JoinUnion<'foo'>; +// => "foo" + +type T4 = JoinUnion; +// => "" +``` + +@see Join +@category Union +@category Template literal +*/ +export type JoinUnion< + Items extends JoinableItem, + Delimiter extends string = ',', +> = UnionToTuple extends infer Tuple extends JoinableItem[] + ? Join + : ''; diff --git a/test-d/join-union.ts b/test-d/join-union.ts new file mode 100644 index 000000000..a802f75e7 --- /dev/null +++ b/test-d/join-union.ts @@ -0,0 +1,23 @@ +import {expectAssignable, expectType} from 'tsd'; +import type {JoinUnion} from '../index.d.ts'; + +expectAssignable<'a,b' | 'b,a'>({} as JoinUnion<'a' | 'b'>); +expectAssignable<'1 | 2' | '2 | 1'>({} as JoinUnion<1 | 2, ' | '>); +expectAssignable<'foo'>({} as JoinUnion<'foo'>); +expectAssignable<'42'>({} as JoinUnion<42>); + +expectAssignable<''>({} as JoinUnion); +expectAssignable<''>({} as JoinUnion); +expectAssignable<','>({} as JoinUnion); +expectAssignable<'2,'>({} as JoinUnion); +expectAssignable({} as JoinUnion<'foo' | string>); // Intended `foo,${string}` +// TODO: For now `UnionToTuple` does not handle 'LiteralUnions'. Will be fixed after `ExtractLiterals` type get approved. + +expectType>(''); +expectType>(''); + +expectAssignable<'a-b' | 'b-a'>({} as JoinUnion<'a' | 'b', '-'>); +expectAssignable<'x🔥y' | 'y🔥x'>({} as JoinUnion<'x' | 'y', '🔥'>); +expectAssignable<'12' | '21'>({} as JoinUnion<1 | 2, ''>); + +expectAssignable<'true or false' | 'false or true'>({} as JoinUnion); From b60ed0977780c8b6dd4e85cf25974f5b1f212f97 Mon Sep 17 00:00:00 2001 From: benzaria Date: Sat, 7 Jun 2025 02:40:33 +0100 Subject: [PATCH 05/17] Improved: `LiteralList` - Fix: Corrected behavior when handling infinite-length lists (no longer returns true). - Add: Added comprehensive tests and documentation for `LiteralList`. - Improve: Enhanced error message formatting for clearer diagnostics. --- source/literal-list.d.ts | 159 +++++++++++++++++++++++++++++++++------ test-d/literal-list.ts | 133 ++++++++++++++++---------------- 2 files changed, 204 insertions(+), 88 deletions(-) diff --git a/source/literal-list.d.ts b/source/literal-list.d.ts index 1abbeb251..afe82756e 100644 --- a/source/literal-list.d.ts +++ b/source/literal-list.d.ts @@ -1,34 +1,151 @@ +import type {IfNotAnyOrNever} from './internal/type.d.ts'; import type {UnionToTuple} from './union-to-tuple.d.ts'; +import type {UnknownArray} from './unknown-array.d.ts'; import type {BuildTuple} from './internal/tuple.d.ts'; import type {Join, JoinableItem} from './join.d.ts'; import type {JoinUnion} from './join-union.d.ts'; import type {IsNever} from './is-never.d.ts'; +/** +Create a tuple where each element is the union `U`, with the length equal to the number of members in `U`. + +@example +``` +type T1 = TupleOfUnions<'a' | 'b'> +//=> ['a' | 'b', 'a' | 'b'] + +type T2 = TupleOfUnions<1 | 2 | 3> +//=> [1 | 2 | 3, 1 | 2 | 3, 1 | 2 | 3] +``` +*/ type TupleOfUnions = UnionToTuple['length'] extends infer Length extends number - ? Readonly> + ? BuildTuple : never; -type TupleAsString = `['${ - [T] extends [JoinableItem[]] - ? Join - : JoinUnion -}']`; +/** +Convert a tuple or union type into a string representation. Used for readable error messages in other types. + + - `S`: **separator** between members (`default: ','`) + - `E`: **start** and **end** delimiters of the string (`default: ['', '']`) + +@example +``` +type T1 = TypeAsString<['a', 'b'], ', ', ['[', ']']>; +// => '[a, b]' + +type T2 = TypeAsString<'a' | 'b', ' | '>; +// => 'a | b' +``` +*/ +type TypeAsString = + `${E[0]}${ + [T] extends [readonly JoinableItem[]] + ? Join + : [T] extends [JoinableItem] + ? JoinUnion + : 'unknown' + }${E[1]}`; + +/** Stringify a tuple as `'[a, b]'` */ +type TupleAsString = TypeAsString; + +/** Stringify a union as `'(a | b)[]'` */ +type UnionAsString = TypeAsString; + +/** +Validates a literal Tuple `List` against a required shape `Shap` (which can be a union or a tuple of same unions). + +Returns the tuple `List` if valid, or if these constraints are violated, a descriptive error message is returned as a string literal. + +#### Requirements: + - `List` **must have the same length** as the number of members in `Shap` + - Each member of `Shap` **must appear exactly once** in `List`, **No duplicates allowed** + - The **order does not matter** + +#### Use Cases: + - Ensuring exhaustive lists of options (e.g., all form field names, enum variants) + - Compile-time enforcement of exact permutations without duplicates + - Defining static configuration or table headers that match an enum or union + +@example-types +``` +import type {LiteralList} from 'type-fest'; + +// ✅ OK +type T1 = LiteralList<['a', 'b'], 'a' | 'b'>; +// => ['a', 'b'] + +// ✅ OK +type T2 = LiteralList<[2, 1], 1 | 2>; +// => [2, 1] + +// ❌ Length unmatch +type T3 = LiteralList<['a', 'b', 'c'], 'a' | 'b'>; +// => '(a | b)[], Type [a, b, c] is not the required Length of: 2' + +// ❌ Missing element +type T4 = LiteralList<['a'], 'a' | 'b'>; +// => '(a | b)[], Type [a] is missing Properties: [b]' + +// ❌ Extra element +type T5 = LiteralList<['a', 'e'], 'a' | 'b'>; +// => '(a | b)[], Type [a, e] has extra Properties: [e]' +``` + +@example-function +``` +import type {LiteralList} from 'type-fest'; + +type Union = 'a' | 'b' | 'c'; + +declare function literalList( + list: LiteralList +): typeof list; + +const C1 = literalList(['a', 'b', 'c']); +//=> ['a', 'b', 'c'] + +const C2 = literalList(['c', 'a', 'b']); +//=> ['c', 'a', 'b'] + +const C3 = literalList(['b', 'b', 'b']); // ❌ Errors in Compiler and IDE +//=> '(a | b | c)[], Type [b, b, b] is missing Properties: [a, c]' +``` + +@author benzaria +@category Type Guard +@category Utilities +*/ +export type LiteralList = + ([Shape] extends [UnknownArray] ? Shape : TupleOfUnions) extends infer TupleShape extends UnknownArray + ? IfNotAnyOrNever, UnionAsString>> + : never; + +/** +Internal comparison logic for `LiteralList`. + +Compares `T` and `U`: + + - Validates that the lengths match. + - Then checks for extra or missing elements. + - If mismatch found, returns a readable error string. -type LiteralList< - T extends readonly any[], - U extends readonly any[] | any, +*/ +type _LiteralList< + T extends UnknownArray, + U extends UnknownArray, + TString extends string, + UString extends string, > = ( - ([U] extends [readonly any[]] ? U : TupleOfUnions) extends infer V extends readonly any[] - ? V['length'] extends T['length'] - ? Exclude extends infer TnV - ? Exclude extends infer VnT - ? IsNever extends true - ? IsNever extends true - ? T - : never | `Type ${TupleAsString} is missing Properties: ${TupleAsString}` - : never | `Type ${TupleAsString} has extra Properties: ${TupleAsString}` - : never + T['length'] extends U['length'] // U.length != number, T always finite + ? Exclude extends infer TnU // T not U + ? Exclude extends infer UnT // U not T + ? IsNever extends true // T includes U + ? IsNever extends true // U includes T + ? T // T == U + : never | `${UString}, Type ${TString} is missing Members: ${TupleAsString}` + : never | `${UString}, Type ${TString} has extra Members: ${TupleAsString}` : never - : never | `Type ${TupleAsString} is not the required Length of: ${V['length']}` - : never + : never + : never | `${UString}, Type ${TString} is not the required Length of: ${U['length']}` ); diff --git a/test-d/literal-list.ts b/test-d/literal-list.ts index 96062e1d0..a6012d700 100644 --- a/test-d/literal-list.ts +++ b/test-d/literal-list.ts @@ -1,73 +1,72 @@ import {expectType} from 'tsd'; -import type {LiteralList, TupleOfUnions} from '../source/literal-list.d.ts'; +import type {LiteralList} from '../source/literal-list.d.ts'; +import type {UnknownArray} from '../source/unknown-array.d.ts'; -type Union = 'a' | 'b' | 'c' | 'd'; -type UnionList = TupleOfUnions; +type U1 = 'a' | 'b' | 'c' | 'd'; +type U2 = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; -declare const bar1: ['a', 'b', 'c', 'd']; -declare const bar2: ['a', 'b', 'd', 'c']; -declare const bar3: ['a', 'c', 'b', 'd']; -declare const bar4: ['a', 'c', 'd', 'b']; -declare const bar5: ['a', 'd', 'b', 'c']; -declare const bar6: ['a', 'd', 'c', 'b']; -declare const bar7: ['b', 'a', 'c', 'd']; -declare const bar8: ['b', 'a', 'd', 'c']; -declare const bar9: ['b', 'c', 'a', 'd']; -declare const bar10: ['b', 'c', 'd', 'a']; -declare const bar11: ['b', 'd', 'a', 'c']; -declare const bar12: ['b', 'd', 'c', 'a']; -declare const bar13: ['c', 'a', 'b', 'd']; -declare const bar14: ['c', 'a', 'd', 'b']; -declare const bar15: ['c', 'b', 'a', 'd']; -declare const bar16: ['c', 'b', 'd', 'a']; -declare const bar17: ['c', 'd', 'a', 'b']; -declare const bar18: ['c', 'd', 'b', 'a']; -declare const bar19: ['d', 'a', 'b', 'c']; -declare const bar20: ['d', 'a', 'c', 'b']; -declare const bar21: ['d', 'b', 'a', 'c']; -declare const bar22: ['d', 'b', 'c', 'a']; -declare const bar23: ['d', 'c', 'a', 'b']; -declare const bar24: ['d', 'c', 'b', 'a']; +// ? Should we add this type +type IsLiteralList = + T extends LiteralList + ? true + : false; -expectType(bar1 satisfies LiteralList); -expectType(bar2 satisfies LiteralList); -expectType(bar3 satisfies LiteralList); -expectType(bar4 satisfies LiteralList); -expectType(bar5 satisfies LiteralList); -expectType(bar6 satisfies LiteralList); -expectType(bar7 satisfies LiteralList); -expectType(bar8 satisfies LiteralList); -expectType(bar9 satisfies LiteralList); -expectType(bar10 satisfies LiteralList); -expectType(bar11 satisfies LiteralList); -expectType(bar12 satisfies LiteralList); -expectType(bar13 satisfies LiteralList); -expectType(bar14 satisfies LiteralList); -expectType(bar15 satisfies LiteralList); -expectType(bar16 satisfies LiteralList); -expectType(bar17 satisfies LiteralList); -expectType(bar18 satisfies LiteralList); -expectType(bar19 satisfies LiteralList); -expectType(bar20 satisfies LiteralList); -expectType(bar21 satisfies LiteralList); -expectType(bar22 satisfies LiteralList); -expectType(bar23 satisfies LiteralList); -expectType(bar24 satisfies LiteralList); +// Base +expectType>(false); +expectType>(false); +expectType>(true); // Should match +expectType>(false); +expectType>(false); +expectType>(true); +expectType>(false); +expectType>(false); +expectType>({} as any); // `any` can't match +expectType>({} as never); // `never` can't match -declare const foo1: ['a', 'b', 'c']; -declare const foo2: ['b', 'c', 'd']; -declare const foo3: ['c', 'a', 'd', 'b', 'f']; -declare const foo4: ['c', 'd', 'e', 'b', 'a']; -declare const foo5: ['a', 'd', 'b', 'b']; -declare const foo6: ['a', 'a', 'c', 'b']; -declare const foo7: ['b', 'a', 'c', 'm']; -declare const foo8: ['b', 'c', 'e', 'd']; +// Orders +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); -expectType<'Type [\'a\', \'b\', \'c\'] is not the required Length of: 4'>({} as LiteralList); -expectType<'Type [\'b\', \'c\', \'d\'] is not the required Length of: 4'>({} as LiteralList); -expectType<'Type [\'c\', \'a\', \'d\', \'b\', \'f\'] is not the required Length of: 4'>({} as LiteralList); -expectType<'Type [\'c\', \'d\', \'e\', \'b\', \'a\'] is not the required Length of: 4'>({} as LiteralList); -expectType<'Type [\'a\', \'d\', \'b\', \'b\'] is missing Properties: [\'c\']'>({} as LiteralList); -expectType<'Type [\'a\', \'a\', \'c\', \'b\'] is missing Properties: [\'d\']'>({} as LiteralList); -expectType<'Type [\'b\', \'a\', \'c\', \'m\'] has extra Properties: [\'m\']'>({} as LiteralList); -expectType<'Type [\'b\', \'c\', \'e\', \'d\'] has extra Properties: [\'e\']'>({} as LiteralList); +// Unions +expectType>(true); +expectType>({} as boolean); +expectType>(false); + +// Long Unions +expectType>(true); // Match +expectType>(false); // Shorter +expectType>(false); // Extra +expectType>(false); // Missing +expectType>(false); // Longer + +// Errors +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); From 40723861f84e3284ec498b3d5f95380069d57a3b Mon Sep 17 00:00:00 2001 From: benzaria Date: Sat, 7 Jun 2025 02:47:56 +0100 Subject: [PATCH 06/17] doc: adding documentation and public exports --- index.d.ts | 2 ++ readme.md | 2 ++ source/internal/type.d.ts | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index f909a8641..542ec7ba6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -39,6 +39,7 @@ export type {PartialOnUndefinedDeep, PartialOnUndefinedDeepOptions} from './sour export type {UndefinedOnPartialDeep} from './source/undefined-on-partial-deep.d.ts'; export type {ReadonlyDeep} from './source/readonly-deep.d.ts'; export type {LiteralUnion} from './source/literal-union.d.ts'; +export type {LiteralList} from './source/literal-list.d.ts'; export type {Promisable} from './source/promisable.d.ts'; export type {Arrayable} from './source/arrayable.d.ts'; export type {Opaque, UnwrapOpaque, Tagged, GetTagMetadata, UnwrapTagged} from './source/tagged.d.ts'; @@ -169,6 +170,7 @@ export type {DelimiterCase} from './source/delimiter-case.d.ts'; export type {DelimiterCasedProperties} from './source/delimiter-cased-properties.d.ts'; export type {DelimiterCasedPropertiesDeep} from './source/delimiter-cased-properties-deep.d.ts'; export type {Join} from './source/join.d.ts'; +export type {JoinUnion} from './source/join-union.d.ts'; export type {Split} from './source/split.d.ts'; export type {Words} from './source/words.d.ts'; export type {Trim} from './source/trim.d.ts'; diff --git a/readme.md b/readme.md index 128957b5d..dd5dd0262 100644 --- a/readme.md +++ b/readme.md @@ -179,6 +179,7 @@ Click the type names for complete docs. - [`And`](source/and.d.ts) - Returns a boolean for whether two given types are both true. - [`Or`](source/or.d.ts) - Returns a boolean for whether either of two given types are true. - [`AllExtend`](source/all-extend.d.ts) - Returns a boolean for whether every element in an array type extends another type. +- [`JoinUnion`](source/join-union.d.ts) - Join an union of [`JoinableItems`](source/join.d.ts#L2) using the given string as a delimiter (default: `,`). - [`NonEmptyTuple`](source/non-empty-tuple.d.ts) - Matches any non-empty tuple. - [`NonEmptyString`](source/non-empty-string.d.ts) - Matches any non-empty string. - [`FindGlobalType`](source/find-global-type.d.ts) - Tries to find the type of a global with the given name. @@ -203,6 +204,7 @@ Click the type names for complete docs. - [`IsUnion`](source/is-union.d.ts) - Returns a boolean for whether the given type is a union. - [`IsLowercase`](source/is-lowercase.d.ts) - Returns a boolean for whether the given string literal is lowercase. - [`IsUppercase`](source/is-uppercase.d.ts) - Returns a boolean for whether the given string literal is uppercase. +- [`LiteralList`](source/literal-list.d.ts) - Validates a literal Tuple `List` against a required shape `Shap` (which can be a union or a tuple of same unions). Returns the tuple `List` if valid, else and Error string. ### JSON diff --git a/source/internal/type.d.ts b/source/internal/type.d.ts index 5ef7823fc..fb28cae01 100644 --- a/source/internal/type.d.ts +++ b/source/internal/type.d.ts @@ -100,7 +100,7 @@ type C = IfNotAnyOrNever; export type IfNotAnyOrNever = If, IfAny, If, IfNever, IfNotAnyOrNever>>; -/* +/** Indicates the value of `exactOptionalPropertyTypes` compiler option. */ export type IsExactOptionalPropertyTypesEnabled = [(string | undefined)?] extends [string?] From bb8e680c223005d3f4a8882115a61f05d80b0be7 Mon Sep 17 00:00:00 2001 From: benzaria Date: Sat, 7 Jun 2025 02:55:08 +0100 Subject: [PATCH 07/17] doc: fix wrong examples --- source/literal-list.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/literal-list.d.ts b/source/literal-list.d.ts index afe82756e..094fd3d11 100644 --- a/source/literal-list.d.ts +++ b/source/literal-list.d.ts @@ -102,13 +102,13 @@ declare function literalList( list: LiteralList ): typeof list; -const C1 = literalList(['a', 'b', 'c']); +const C1 = literalList(['a', 'b', 'c'] as const); //=> ['a', 'b', 'c'] -const C2 = literalList(['c', 'a', 'b']); +const C2 = literalList(['c', 'a', 'b'] as const); //=> ['c', 'a', 'b'] -const C3 = literalList(['b', 'b', 'b']); // ❌ Errors in Compiler and IDE +const C3 = literalList(['b', 'b', 'b'] as const); // ❌ Errors in Compiler and IDE //=> '(a | b | c)[], Type [b, b, b] is missing Properties: [a, c]' ``` From d7a6def563a96dbb9c7778e53079b1a119a4d3c7 Mon Sep 17 00:00:00 2001 From: benzaria Date: Sat, 7 Jun 2025 03:15:12 +0100 Subject: [PATCH 08/17] doc: fix wrong examples --- source/literal-list.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/literal-list.d.ts b/source/literal-list.d.ts index 094fd3d11..e69cad044 100644 --- a/source/literal-list.d.ts +++ b/source/literal-list.d.ts @@ -98,7 +98,7 @@ import type {LiteralList} from 'type-fest'; type Union = 'a' | 'b' | 'c'; -declare function literalList( +declare function literalList( list: LiteralList ): typeof list; From 77d29a99e95f46a588953d08dd484d087157564d Mon Sep 17 00:00:00 2001 From: benzaria Date: Mon, 9 Jun 2025 18:44:01 +0100 Subject: [PATCH 09/17] doc: improve JsDoc for `JoinUnion` --- source/join-union.d.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/source/join-union.d.ts b/source/join-union.d.ts index 7a5b7d381..d342d9c27 100644 --- a/source/join-union.d.ts +++ b/source/join-union.d.ts @@ -2,23 +2,25 @@ import type {UnionToTuple} from './union-to-tuple.d.ts'; import type {Join, JoinableItem} from './join.d.ts'; /** -Join an union of {@link JoinableItem `JoinableItems`} using the given string as a delimiter (default: `,`). +Join an union of {@link JoinableItem `JoinableItems`} using the given string as a delimiter. + +Delimiter defaults to `,`. @example ``` import type {JoinUnion} from 'type-fest'; type T1 = JoinUnion<'a' | 'b' | 'c'>; -// => "a, b, c" +//=> 'a, b, c' type T2 = JoinUnion<1 | 2 | 3, ' | '>; -// => "1 | 2 | 3" +//=> '1 | 2 | 3' type T3 = JoinUnion<'foo'>; -// => "foo" +//=> 'foo' type T4 = JoinUnion; -// => "" +//=> '' ``` @see Join From f21ee18ce71ea66d80583f013dcf03563d808b80 Mon Sep 17 00:00:00 2001 From: benzaria Date: Mon, 9 Jun 2025 18:46:57 +0100 Subject: [PATCH 10/17] feat: improve `TypeAsString` to support 1 depth arrays and refactor JsDoc for `LiteralList` --- source/literal-list.d.ts | 45 ++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/source/literal-list.d.ts b/source/literal-list.d.ts index e69cad044..833ee1f65 100644 --- a/source/literal-list.d.ts +++ b/source/literal-list.d.ts @@ -5,22 +5,21 @@ import type {BuildTuple} from './internal/tuple.d.ts'; import type {Join, JoinableItem} from './join.d.ts'; import type {JoinUnion} from './join-union.d.ts'; import type {IsNever} from './is-never.d.ts'; +import type {IsUnion} from './is-union.d.ts'; /** Create a tuple where each element is the union `U`, with the length equal to the number of members in `U`. @example ``` -type T1 = TupleOfUnions<'a' | 'b'> +type T1 = TupleOfUnions<'a' | 'b'>; //=> ['a' | 'b', 'a' | 'b'] -type T2 = TupleOfUnions<1 | 2 | 3> +type T2 = TupleOfUnions<1 | 2 | 3>; //=> [1 | 2 | 3, 1 | 2 | 3, 1 | 2 | 3] ``` */ -type TupleOfUnions = UnionToTuple['length'] extends infer Length extends number - ? BuildTuple - : never; +type TupleOfUnions = BuildTuple['length'], U>; /** Convert a tuple or union type into a string representation. Used for readable error messages in other types. @@ -31,19 +30,22 @@ Convert a tuple or union type into a string representation. Used for readable er @example ``` type T1 = TypeAsString<['a', 'b'], ', ', ['[', ']']>; -// => '[a, b]' +//=> '[a, b]' type T2 = TypeAsString<'a' | 'b', ' | '>; -// => 'a | b' +//=> 'a | b' ``` */ +// TODO: Make a separate `Stringify` type for `JoinableItem[]` mixed with `JoinableItem` type TypeAsString = `${E[0]}${ - [T] extends [readonly JoinableItem[]] - ? Join + [T] extends [readonly JoinableItem[]] // TODO: add `JoinableArray` type + ? IsUnion extends true + ? JoinUnion<`[${Join}]`, S> + : Join : [T] extends [JoinableItem] ? JoinUnion - : 'unknown' + : '...' // To Complex }${E[1]}`; /** Stringify a tuple as `'[a, b]'` */ @@ -53,7 +55,7 @@ type TupleAsString = TypeAsString; type UnionAsString = TypeAsString; /** -Validates a literal Tuple `List` against a required shape `Shap` (which can be a union or a tuple of same unions). +Validates a literal Tuple `List` against a required union `Shap`. Returns the tuple `List` if valid, or if these constraints are violated, a descriptive error message is returned as a string literal. @@ -67,32 +69,32 @@ Returns the tuple `List` if valid, or if these constraints are violated, a descr - Compile-time enforcement of exact permutations without duplicates - Defining static configuration or table headers that match an enum or union -@example-types +@example ``` import type {LiteralList} from 'type-fest'; // ✅ OK type T1 = LiteralList<['a', 'b'], 'a' | 'b'>; -// => ['a', 'b'] +//=> ['a', 'b'] // ✅ OK type T2 = LiteralList<[2, 1], 1 | 2>; -// => [2, 1] +//=> [2, 1] // ❌ Length unmatch type T3 = LiteralList<['a', 'b', 'c'], 'a' | 'b'>; -// => '(a | b)[], Type [a, b, c] is not the required Length of: 2' +//=> '(a | b)[], Type [a, b, c] is not the required Length of: 2' // ❌ Missing element type T4 = LiteralList<['a'], 'a' | 'b'>; -// => '(a | b)[], Type [a] is missing Properties: [b]' +//=> '(a | b)[], Type [a] is missing Properties: [b]' // ❌ Extra element type T5 = LiteralList<['a', 'e'], 'a' | 'b'>; -// => '(a | b)[], Type [a, e] has extra Properties: [e]' +//=> '(a | b)[], Type [a, e] has extra Properties: [e]' ``` -@example-function +@example ``` import type {LiteralList} from 'type-fest'; @@ -112,17 +114,14 @@ const C3 = literalList(['b', 'b', 'b'] as const); // ❌ Errors in Compiler and //=> '(a | b | c)[], Type [b, b, b] is missing Properties: [a, c]' ``` -@author benzaria @category Type Guard @category Utilities */ export type LiteralList = - ([Shape] extends [UnknownArray] ? Shape : TupleOfUnions) extends infer TupleShape extends UnknownArray - ? IfNotAnyOrNever, UnionAsString>> - : never; + IfNotAnyOrNever, TupleAsString, UnionAsString>>; /** -Internal comparison logic for `LiteralList`. +Internal comparison logic for {@link LiteralList `LiteralList`}. Compares `T` and `U`: From ea13d94e5e618920a44b53a7886c538c66df04a1 Mon Sep 17 00:00:00 2001 From: benzaria Date: Mon, 9 Jun 2025 18:47:44 +0100 Subject: [PATCH 11/17] feat: add tests covering array `Shape` union --- test-d/literal-list.ts | 49 +++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/test-d/literal-list.ts b/test-d/literal-list.ts index a6012d700..2759a3459 100644 --- a/test-d/literal-list.ts +++ b/test-d/literal-list.ts @@ -1,9 +1,10 @@ -import {expectType} from 'tsd'; +import {expectAssignable, expectType} from 'tsd'; import type {LiteralList} from '../source/literal-list.d.ts'; import type {UnknownArray} from '../source/unknown-array.d.ts'; type U1 = 'a' | 'b' | 'c' | 'd'; type U2 = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; +type U3 = ['a'] | ['b', 'c'] | ['a', 'b']; // ? Should we add this type type IsLiteralList = @@ -61,12 +62,40 @@ expectType>(false); // Extra expectType>(false); // Missing expectType>(false); // Longer -// Errors -expectType>(false); -expectType>(false); -expectType>(false); -expectType>(false); -expectType>(false); -expectType>(false); -expectType>(false); -expectType>(false); +// Errors for `JoinableItem` (hover to see errors) +type I1 = LiteralList<['a', 'b', 'c'], U1>; +type I2 = LiteralList<['b', 'c', 'd'], U1>; +type I3 = LiteralList<['c', 'a', 'd', 'b', 'f'], U1>; +type I4 = LiteralList<['c', 'd', 'e', 'b', 'a'], U1>; +type I5 = LiteralList<['a', 'd', 'b', 'b'], U1>; +type I6 = LiteralList<['a', 'a', 'b', 'b'], U1>; +type I7 = LiteralList<['b', 'a', 'c', 'm'], U1>; +type I8 = LiteralList<['b', 'c', 'e', 'm'], U1>; + +expectAssignable({} as I1); +expectAssignable({} as I2); +expectAssignable({} as I3); +expectAssignable({} as I4); +expectAssignable({} as I5); +expectAssignable({} as I6); +expectAssignable({} as I7); +expectAssignable({} as I8); + +// Errors for `JoinableItem[]` (hover to see errors) +type A1 = LiteralList<[['a'], ['b', 'c']], U3>; +type A2 = LiteralList<[['b', 'c'], ['a', 'b']], U3>; +type A3 = LiteralList<[['a'], ['b', 'c'], ['a', 'b'], ['f']], U3>; +type A4 = LiteralList<[['a'], ['b', 'c'], ['a', 'b'], 'd'], U3>; +type A5 = LiteralList<[['b', 'c'], ['a'], ['a']], U3>; +type A6 = LiteralList<[['a'], ['b', 'c'], ['b', 'c']], U3>; +type A7 = LiteralList<[['a'], ['b', 'c'], ['f']], U3>; +type A8 = LiteralList<[['b'], ['e'], ['a', 'b']], U3>; + +expectAssignable({} as A1); +expectAssignable({} as A2); +expectAssignable({} as A3); +expectAssignable({} as A4); +expectAssignable({} as A5); +expectAssignable({} as A6); +expectAssignable({} as A7); +expectAssignable({} as A8); From ca44c5318a4d02e57c1dcf2bc8da8b3fe7e6f4dc Mon Sep 17 00:00:00 2001 From: benzaria Date: Mon, 9 Jun 2025 18:48:38 +0100 Subject: [PATCH 12/17] doc: change description for `LiteralList` --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index dd5dd0262..e82bce6f9 100644 --- a/readme.md +++ b/readme.md @@ -204,7 +204,7 @@ Click the type names for complete docs. - [`IsUnion`](source/is-union.d.ts) - Returns a boolean for whether the given type is a union. - [`IsLowercase`](source/is-lowercase.d.ts) - Returns a boolean for whether the given string literal is lowercase. - [`IsUppercase`](source/is-uppercase.d.ts) - Returns a boolean for whether the given string literal is uppercase. -- [`LiteralList`](source/literal-list.d.ts) - Validates a literal Tuple `List` against a required shape `Shap` (which can be a union or a tuple of same unions). Returns the tuple `List` if valid, else and Error string. +- [`LiteralList`](source/literal-list.d.ts) - Validates a literal Tuple `List` against a required union `Shap`. Returns `List` if valid, else an Error string. ### JSON From 01ae42a7d8e5d719b9826de8808e5235ea5d66be Mon Sep 17 00:00:00 2001 From: benzaria Date: Mon, 9 Jun 2025 18:56:23 +0100 Subject: [PATCH 13/17] revert unwanted changes on `TupleOfUnions` --- source/literal-list.d.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/source/literal-list.d.ts b/source/literal-list.d.ts index 833ee1f65..ed4a57c2c 100644 --- a/source/literal-list.d.ts +++ b/source/literal-list.d.ts @@ -19,7 +19,9 @@ type T2 = TupleOfUnions<1 | 2 | 3>; //=> [1 | 2 | 3, 1 | 2 | 3, 1 | 2 | 3] ``` */ -type TupleOfUnions = BuildTuple['length'], U>; +type TupleOfUnions = UnionToTuple['length'] extends infer Length extends number + ? BuildTuple + : never; /** Convert a tuple or union type into a string representation. Used for readable error messages in other types. @@ -36,7 +38,7 @@ type T2 = TypeAsString<'a' | 'b', ' | '>; //=> 'a | b' ``` */ -// TODO: Make a separate `Stringify` type for `JoinableItem[]` mixed with `JoinableItem` +// TODO: Make a separate `Stringify` type `JoinableItem[]` mixed with `JoinableItem` type TypeAsString = `${E[0]}${ [T] extends [readonly JoinableItem[]] // TODO: add `JoinableArray` type From 33385d4f5310e2cecc37ea8c969a4cc3389f210d Mon Sep 17 00:00:00 2001 From: benzaria Date: Mon, 9 Jun 2025 18:57:22 +0100 Subject: [PATCH 14/17] doc: change description for `JoinUnion` --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index e82bce6f9..29f5c17de 100644 --- a/readme.md +++ b/readme.md @@ -179,7 +179,7 @@ Click the type names for complete docs. - [`And`](source/and.d.ts) - Returns a boolean for whether two given types are both true. - [`Or`](source/or.d.ts) - Returns a boolean for whether either of two given types are true. - [`AllExtend`](source/all-extend.d.ts) - Returns a boolean for whether every element in an array type extends another type. -- [`JoinUnion`](source/join-union.d.ts) - Join an union of [`JoinableItems`](source/join.d.ts#L2) using the given string as a delimiter (default: `,`). +- [`JoinUnion`](source/join-union.d.ts) - Join a union of strings and/or numbers using the given string as a delimiter. - [`NonEmptyTuple`](source/non-empty-tuple.d.ts) - Matches any non-empty tuple. - [`NonEmptyString`](source/non-empty-string.d.ts) - Matches any non-empty string. - [`FindGlobalType`](source/find-global-type.d.ts) - Tries to find the type of a global with the given name. From 48efb44075c7abdf444498d33cc830dc3efdaa96 Mon Sep 17 00:00:00 2001 From: benzaria Date: Tue, 10 Jun 2025 21:24:04 +0100 Subject: [PATCH 15/17] doc: fix typos & improve JsDoc clarity --- readme.md | 2 +- source/join-union.d.ts | 2 +- source/literal-list.d.ts | 20 ++++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/readme.md b/readme.md index 29f5c17de..01e9c4bc0 100644 --- a/readme.md +++ b/readme.md @@ -204,7 +204,7 @@ Click the type names for complete docs. - [`IsUnion`](source/is-union.d.ts) - Returns a boolean for whether the given type is a union. - [`IsLowercase`](source/is-lowercase.d.ts) - Returns a boolean for whether the given string literal is lowercase. - [`IsUppercase`](source/is-uppercase.d.ts) - Returns a boolean for whether the given string literal is uppercase. -- [`LiteralList`](source/literal-list.d.ts) - Validates a literal Tuple `List` against a required union `Shap`. Returns `List` if valid, else an Error string. +- [`LiteralList`](source/literal-list.d.ts) - Enforces that a tuple contains exactly the members of a union type, with no duplicates or omissions. ### JSON diff --git a/source/join-union.d.ts b/source/join-union.d.ts index d342d9c27..f940c9923 100644 --- a/source/join-union.d.ts +++ b/source/join-union.d.ts @@ -2,7 +2,7 @@ import type {UnionToTuple} from './union-to-tuple.d.ts'; import type {Join, JoinableItem} from './join.d.ts'; /** -Join an union of {@link JoinableItem `JoinableItems`} using the given string as a delimiter. +Join a union of strings and/or numbers ({@link JoinableItem `JoinableItems`}) using the given string as a delimiter. Delimiter defaults to `,`. diff --git a/source/literal-list.d.ts b/source/literal-list.d.ts index ed4a57c2c..d8a39bae5 100644 --- a/source/literal-list.d.ts +++ b/source/literal-list.d.ts @@ -38,7 +38,7 @@ type T2 = TypeAsString<'a' | 'b', ' | '>; //=> 'a | b' ``` */ -// TODO: Make a separate `Stringify` type `JoinableItem[]` mixed with `JoinableItem` +// TODO: Make a separate `Stringify` type for `JoinableItem[]` mixed with `JoinableItem` type TypeAsString = `${E[0]}${ [T] extends [readonly JoinableItem[]] // TODO: add `JoinableArray` type @@ -47,7 +47,7 @@ type TypeAsString : [T] extends [JoinableItem] ? JoinUnion - : '...' // To Complex + : '...' // Too complex }${E[1]}`; /** Stringify a tuple as `'[a, b]'` */ @@ -57,13 +57,13 @@ type TupleAsString = TypeAsString; type UnionAsString = TypeAsString; /** -Validates a literal Tuple `List` against a required union `Shap`. +Enforces that a tuple contains exactly the members of a union type, with no duplicates or omissions. -Returns the tuple `List` if valid, or if these constraints are violated, a descriptive error message is returned as a string literal. +Returns the tuple `List` if valid. Otherwise, if any constraints are violated, a descriptive error message is returned as a string literal. #### Requirements: - - `List` **must have the same length** as the number of members in `Shap` - - Each member of `Shap` **must appear exactly once** in `List`, **No duplicates allowed** + - `List` **must have the same length** as the number of members in `Shape` + - Each member of `Shape` **must appear exactly once** in `List`, **No duplicates allowed** - The **order does not matter** #### Use Cases: @@ -83,17 +83,17 @@ type T1 = LiteralList<['a', 'b'], 'a' | 'b'>; type T2 = LiteralList<[2, 1], 1 | 2>; //=> [2, 1] -// ❌ Length unmatch +// ❌ Length mismatch type T3 = LiteralList<['a', 'b', 'c'], 'a' | 'b'>; //=> '(a | b)[], Type [a, b, c] is not the required Length of: 2' // ❌ Missing element type T4 = LiteralList<['a'], 'a' | 'b'>; -//=> '(a | b)[], Type [a] is missing Properties: [b]' +//=> '(a | b)[], Type [a] is missing Members: [b]' // ❌ Extra element type T5 = LiteralList<['a', 'e'], 'a' | 'b'>; -//=> '(a | b)[], Type [a, e] has extra Properties: [e]' +//=> '(a | b)[], Type [a, e] has extra Members: [e]' ``` @example @@ -113,7 +113,7 @@ const C2 = literalList(['c', 'a', 'b'] as const); //=> ['c', 'a', 'b'] const C3 = literalList(['b', 'b', 'b'] as const); // ❌ Errors in Compiler and IDE -//=> '(a | b | c)[], Type [b, b, b] is missing Properties: [a, c]' +//=> '(a | b | c)[], Type [b, b, b] is missing Members: [a, c]' ``` @category Type Guard From 750004a3dd643203377ef2dfabd0ba62e93a31d5 Mon Sep 17 00:00:00 2001 From: benz Date: Sun, 15 Jun 2025 00:47:41 +0000 Subject: [PATCH 16/17] feat: remove capitals from Errors --- source/literal-list.d.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/source/literal-list.d.ts b/source/literal-list.d.ts index d8a39bae5..cedf87d17 100644 --- a/source/literal-list.d.ts +++ b/source/literal-list.d.ts @@ -38,7 +38,7 @@ type T2 = TypeAsString<'a' | 'b', ' | '>; //=> 'a | b' ``` */ -// TODO: Make a separate `Stringify` type for `JoinableItem[]` mixed with `JoinableItem` +// TODO: Make a separate `Stringify` type type TypeAsString = `${E[0]}${ [T] extends [readonly JoinableItem[]] // TODO: add `JoinableArray` type @@ -85,15 +85,15 @@ type T2 = LiteralList<[2, 1], 1 | 2>; // ❌ Length mismatch type T3 = LiteralList<['a', 'b', 'c'], 'a' | 'b'>; -//=> '(a | b)[], Type [a, b, c] is not the required Length of: 2' +//=> '(a | b)[], Type [a, b, c] is not the required length of: 2' // ❌ Missing element type T4 = LiteralList<['a'], 'a' | 'b'>; -//=> '(a | b)[], Type [a] is missing Members: [b]' +//=> '(a | b)[], Type [a] is missing members: [b]' // ❌ Extra element type T5 = LiteralList<['a', 'e'], 'a' | 'b'>; -//=> '(a | b)[], Type [a, e] has extra Members: [e]' +//=> '(a | b)[], Type [a, e] has extra members: [e]' ``` @example @@ -113,7 +113,7 @@ const C2 = literalList(['c', 'a', 'b'] as const); //=> ['c', 'a', 'b'] const C3 = literalList(['b', 'b', 'b'] as const); // ❌ Errors in Compiler and IDE -//=> '(a | b | c)[], Type [b, b, b] is missing Members: [a, c]' +//=> '(a | b | c)[], Type [b, b, b] is missing members: [a, c]' ``` @category Type Guard @@ -144,9 +144,9 @@ type _LiteralList< ? IsNever extends true // T includes U ? IsNever extends true // U includes T ? T // T == U - : never | `${UString}, Type ${TString} is missing Members: ${TupleAsString}` - : never | `${UString}, Type ${TString} has extra Members: ${TupleAsString}` + : never | `${UString}, Type ${TString} is missing members: ${TupleAsString}` + : never | `${UString}, Type ${TString} has extra members: ${TupleAsString}` : never : never - : never | `${UString}, Type ${TString} is not the required Length of: ${U['length']}` + : never | `${UString}, Type ${TString} is not the required length of: ${U['length']}` ); From 15dab31def4927bf8e2ad32239176dd9fa0d395a Mon Sep 17 00:00:00 2001 From: benzaria Date: Sun, 15 Jun 2025 20:37:21 +0100 Subject: [PATCH 17/17] test: add test case for literal template & fix literalunion test --- test-d/join-union.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test-d/join-union.ts b/test-d/join-union.ts index a802f75e7..e48b64a40 100644 --- a/test-d/join-union.ts +++ b/test-d/join-union.ts @@ -10,7 +10,9 @@ expectAssignable<''>({} as JoinUnion); expectAssignable<''>({} as JoinUnion); expectAssignable<','>({} as JoinUnion); expectAssignable<'2,'>({} as JoinUnion); -expectAssignable({} as JoinUnion<'foo' | string>); // Intended `foo,${string}` + +expectAssignable<`foo,on${string}` | `on${string},foo`>({} as JoinUnion<'foo' | `on${string}`>); +expectAssignable<`${string & {}}`>({} as JoinUnion<'foo' | (string & {})>); // Intended `foo,${string}` // TODO: For now `UnionToTuple` does not handle 'LiteralUnions'. Will be fixed after `ExtractLiterals` type get approved. expectType>('');