-
-
Notifications
You must be signed in to change notification settings - Fork 672
Add LiteralList and JoinUnion types
#1159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
benzaria
wants to merge
31
commits into
sindresorhus:main
Choose a base branch
from
benzaria:LiteralList
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 29 commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
3e327f7
Add `LiteralList` and `JoinUnion` types
benzaria cdc113c
Fix: test errors
benzaria fdbc3e5
Merge branch 'main' into LiteralList
benzaria 452e4fc
reverte changes on `Join`
benzaria d90091b
Add: `JoinUnion` tests, docs
benzaria b60ed09
Improved: `LiteralList`
benzaria 4072386
doc: adding documentation and public exports
benzaria bb8e680
doc: fix wrong examples
benzaria d7a6def
doc: fix wrong examples
benzaria 77d29a9
doc: improve JsDoc for `JoinUnion`
benzaria f21ee18
feat: improve `TypeAsString` to support 1 depth arrays and refactor J…
benzaria ea13d94
feat: add tests covering array `Shape` union
benzaria ca44c53
doc: change description for `LiteralList`
benzaria 193fc9f
Merge branch 'main' into LiteralList
benzaria 01ae42a
revert unwanted changes on `TupleOfUnions`
benzaria 33385d4
doc: change description for `JoinUnion`
benzaria 48efb44
doc: fix typos & improve JsDoc clarity
benzaria 11ca817
Merge branch 'main' into LiteralList
benzaria 8e3127c
Merge branch 'main' into LiteralList
benzaria 750004a
feat: remove capitals from Errors
benzaria 15dab31
test: add test case for literal template & fix literalunion test
benzaria 0c6d715
Merge branch 'main' into LiteralList
benzaria fa55556
Merge branch 'main' into LiteralList
benzaria f435fa5
refactor: `LiteralList` and `JoinUnion` types
benzaria f260b64
feat: minor changes
benzaria af81efe
Merge remote-tracking branch 'upstream/main' into LiteralList
benzaria 659fb1f
fix `UnionToTuple` type output not treated as an array
benzaria c8d83a1
fix errors caused by new rules
benzaria 53fecf9
revert changes on `ArrayLength`
benzaria 2a32fdc
adding note to `JoinUnion`
benzaria dac0010
fixing JsDoc example on `JoinUnion`
benzaria File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import type {UnionToTuple} from './union-to-tuple.d.ts'; | ||
| import type {Join, JoinableItem} from './join.d.ts'; | ||
|
|
||
| /** | ||
| Join a union of strings and/or numbers ({@link JoinableItem `JoinableItems`}) using the given string as a delimiter. | ||
|
|
||
| Delimiter defaults to `,`. | ||
|
|
||
benzaria marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| @example | ||
| ``` | ||
| import type {JoinUnion} from 'type-fest'; | ||
|
|
||
| type T1 = JoinUnion<'a' | 'b' | 'c'>; | ||
| //=> 'a, b, c' | ||
benzaria marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| type T2 = JoinUnion<1 | 2 | 3, ' | '>; | ||
| //=> '1 | 2 | 3' | ||
|
|
||
| type T3 = JoinUnion<'foo'>; | ||
| //=> 'foo' | ||
|
|
||
| type T4 = JoinUnion<never>; | ||
| //=> '' | ||
| ``` | ||
|
|
||
| @see {@link Join} | ||
| @category Union | ||
| @category Template literal | ||
| */ | ||
| export type JoinUnion< | ||
| Items extends JoinableItem, | ||
| Delimiter extends string = ',', | ||
| > = UnionToTuple<Items> extends infer Tuple extends JoinableItem[] | ||
| ? Join<Tuple, Delimiter> | ||
| : ''; | ||
|
|
||
| export {}; | ||
benzaria marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| 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 {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'; | ||
|
|
||
| /** | ||
| 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' | ||
| ``` | ||
| */ | ||
| // TODO: Make a separate `Stringify` type | ||
| type TypeAsString<T, S extends string = ',', E extends [string, string] = ['', '']> = | ||
| `${E[0]}${ | ||
| [T] extends [readonly JoinableItem[]] // TODO: add `JoinableArray` type | ||
| ? IsUnion<T> extends true | ||
| ? JoinUnion<`[${Join<T, ', '>}]`, S> | ||
| : Join<T, S> | ||
| : [T] extends [JoinableItem] | ||
| ? JoinUnion<T, S> | ||
| : '...' // Too complex | ||
| }${E[1]}`; | ||
|
|
||
| /** Stringify a tuple as `'[a, b]'` */ | ||
| type TupleAsString<T> = TypeAsString<T, ', ', ['[', ']']>; | ||
|
|
||
| /** Stringify a union as `'(a | b)[]'` */ | ||
| type UnionAsString<U> = TypeAsString<U, ' | ', ['(', ')[]']>; | ||
|
|
||
| /** | ||
| Enforces that a tuple contains exactly the members of a union type, with no duplicates or omissions. | ||
|
|
||
| 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 `Shape` | ||
| - Each member of `Shape` **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 | ||
| ``` | ||
| 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 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 members: [b]' | ||
|
|
||
| // ❌ Extra element | ||
| type T5 = LiteralList<['a', 'e'], 'a' | 'b'>; | ||
| //=> '(a | b)[], Type [a, e] has extra members: [e]' | ||
| ``` | ||
|
|
||
| @example | ||
| ``` | ||
| import type {LiteralList} from 'type-fest'; | ||
|
|
||
| type Union = 'a' | 'b' | 'c'; | ||
|
|
||
| declare function literalList<const T extends UnknownArray>( | ||
| list: LiteralList<T, Union> | ||
| ): typeof list; | ||
|
|
||
| const C1 = literalList(['a', 'b', 'c'] as const); | ||
| //=> ['a', 'b', 'c'] | ||
|
|
||
| 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]' | ||
| ``` | ||
|
|
||
| @category Type Guard | ||
| @category Utilities | ||
| */ | ||
| export type LiteralList<List extends UnknownArray, Shape extends UnknownArray | unknown> = | ||
| IfNotAnyOrNever<List, | ||
| _LiteralList< | ||
| List, Shape, | ||
| UnionToTuple<Shape>['length'], | ||
| TupleAsString<List>, | ||
| UnionAsString<Shape> | ||
| > | ||
| >; | ||
|
|
||
| /** | ||
| Internal comparison logic for {@link LiteralList `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 UnknownArray, U, | ||
| ULength extends number, | ||
| TString extends string, | ||
| UString extends string, | ||
| > = ( | ||
| T['length'] extends ULength // U.length != number, T always finite | ||
| ? Exclude<T[number], U> extends infer TnU // T not U | ||
| ? Exclude<U, T[number]> extends infer UnT // U not T | ||
| ? IsNever<TnU> extends true // T includes U | ||
| ? IsNever<UnT> extends true // U includes T | ||
| ? T // T is U | ||
| : never | `${UString}, Type ${TString} is missing members: ${TupleAsString<UnT>}` | ||
| : never | `${UString}, Type ${TString} has extra members: ${TupleAsString<TnU>}` | ||
| : never | ||
| : never | ||
| : never | `${UString}, Type ${TString} is not the required length of: ${ULength}` | ||
| ); | ||
|
|
||
| export {}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import {expectAssignable, expectType} from 'tsd'; | ||
| import type {JoinUnion} from '../source/join-union.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<null>); | ||
| expectAssignable<''>({} as JoinUnion<undefined>); | ||
| expectAssignable<','>({} as JoinUnion<undefined | null>); | ||
| expectAssignable<'2,'>({} as JoinUnion<undefined | 2>); | ||
|
|
||
| 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<JoinUnion<never>>(''); | ||
| expectType<JoinUnion<never, ' + '>>(''); | ||
|
|
||
| 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<boolean, ' or '>); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| 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']; | ||
|
|
||
| type IsLiteralList<T extends UnknownArray, U> = | ||
| T extends LiteralList<T, U> | ||
| ? true | ||
| : false; | ||
|
|
||
| // Base | ||
| expectType<IsLiteralList<[], U1>>(false); | ||
| expectType<IsLiteralList<U1[], U1>>(false); | ||
| expectType<IsLiteralList<[U1, U1, U1, U1], U1>>(true); | ||
| expectType<IsLiteralList<[U1, U1, U1], U1>>(false); | ||
| expectType<IsLiteralList<[U1, U1, U1, U1, U1], U1>>(false); | ||
| expectType<IsLiteralList<[...['a', 'b', 'd', 'c']], U1>>(true); | ||
| expectType<IsLiteralList<unknown[], U1>>(false); | ||
| expectType<IsLiteralList<[unknown, unknown, unknown, unknown], U1>>(false); | ||
| expectType<LiteralList<any, U1>>({} as any); // `any` can't match | ||
| expectType<LiteralList<never, U1>>({} as never); // `never` can't match | ||
|
|
||
| // Orders | ||
| expectType<IsLiteralList<['a', 'b', 'c', 'd'], U1>>(true); | ||
| expectType<IsLiteralList<['a', 'b', 'd', 'c'], U1>>(true); | ||
| expectType<IsLiteralList<['a', 'c', 'b', 'd'], U1>>(true); | ||
| expectType<IsLiteralList<['a', 'c', 'd', 'b'], U1>>(true); | ||
| expectType<IsLiteralList<['a', 'd', 'b', 'c'], U1>>(true); | ||
| expectType<IsLiteralList<['a', 'd', 'c', 'b'], U1>>(true); | ||
| expectType<IsLiteralList<['b', 'a', 'c', 'd'], U1>>(true); | ||
| expectType<IsLiteralList<['b', 'a', 'd', 'c'], U1>>(true); | ||
| expectType<IsLiteralList<['b', 'c', 'a', 'd'], U1>>(true); | ||
| expectType<IsLiteralList<['b', 'c', 'd', 'a'], U1>>(true); | ||
| expectType<IsLiteralList<['b', 'd', 'a', 'c'], U1>>(true); | ||
| expectType<IsLiteralList<['b', 'd', 'c', 'a'], U1>>(true); | ||
| expectType<IsLiteralList<['c', 'a', 'b', 'd'], U1>>(true); | ||
| expectType<IsLiteralList<['c', 'a', 'd', 'b'], U1>>(true); | ||
| expectType<IsLiteralList<['c', 'b', 'a', 'd'], U1>>(true); | ||
| expectType<IsLiteralList<['c', 'b', 'd', 'a'], U1>>(true); | ||
| expectType<IsLiteralList<['c', 'd', 'a', 'b'], U1>>(true); | ||
| expectType<IsLiteralList<['c', 'd', 'b', 'a'], U1>>(true); | ||
| expectType<IsLiteralList<['d', 'a', 'b', 'c'], U1>>(true); | ||
| expectType<IsLiteralList<['d', 'a', 'c', 'b'], U1>>(true); | ||
| expectType<IsLiteralList<['d', 'b', 'a', 'c'], U1>>(true); | ||
| expectType<IsLiteralList<['d', 'b', 'c', 'a'], U1>>(true); | ||
| expectType<IsLiteralList<['d', 'c', 'a', 'b'], U1>>(true); | ||
| expectType<IsLiteralList<['d', 'c', 'b', 'a'], U1>>(true); | ||
|
|
||
| // Unions | ||
| expectType<IsLiteralList<['a', 'b', 'c', 'd'] | ['a', 'b', 'd', 'c'], U1>>(true); | ||
| expectType<IsLiteralList<['a', 'c', 'b', 'd'] | ['e'], U1>>({} as boolean); | ||
| expectType<IsLiteralList<['a'] | ['e'], U1>>(false); | ||
|
|
||
| // Long Unions | ||
| expectType<IsLiteralList<[1, 2, 3, 4, 5, 6, 7, 8, 9], U2>>(true); // Match | ||
| expectType<IsLiteralList<[1, 2, 3, 4, 5, 6, 7, 8], U2>>(false); // Shorter | ||
| expectType<IsLiteralList<[1, 2, 3, 4, 5, 6, 7, 8, 0], U2>>(false); // Extra | ||
| expectType<IsLiteralList<[1, 2, 3, 4, 5, 6, 7, 8, 8], U2>>(false); // Missing | ||
| expectType<IsLiteralList<[1, 2, 3, 4, 5, 6, 7, 8, 9, 0], U2>>(false); // Longer | ||
|
|
||
| // 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<string>({} as I1); | ||
| expectAssignable<string>({} as I2); | ||
| expectAssignable<string>({} as I3); | ||
| expectAssignable<string>({} as I4); | ||
| expectAssignable<string>({} as I5); | ||
| expectAssignable<string>({} as I6); | ||
| expectAssignable<string>({} as I7); | ||
| expectAssignable<string>({} 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<string>({} as A1); | ||
| expectAssignable<string>({} as A2); | ||
| expectAssignable<string>({} as A3); | ||
| expectAssignable<string>({} as A4); | ||
| expectAssignable<string>({} as A5); | ||
| expectAssignable<string>({} as A6); | ||
| expectAssignable<string>({} as A7); | ||
| expectAssignable<string>({} as A8); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.