Issue
I am looking to join an unknown/unlimited number of numeric strings (with/without px). Using this code, you can only join two items. So, is there a way I can join many items using a space? If not is there a way to do it for up to 12 items easily (preferably recursive and not joining with 12 |
's where another item is added)?
TypeScript Version: 4.9.5
export type Format = `${number}` | `${number}px`;
// Somehow create a join of `Format` separated by a space with unlimited number of values
export type Input = `${Format} `${Format}`;
I am looking to have all of these (and more) validate as a valid string:
const input: Input = '1';
const input: Input = '1 2';
const input: Input = '1 2 3';
const input: Input = '1 2 10px';
const input: Input = '10px 1 2 20px';
// And so on
Solution
You can do this with a recursive conditional type:
type Last<T extends unknown[]> =
T extends [...unknown[], infer U]
? U
: never
type JoinedUnion<
Format extends string,
Limit extends number,
Acc extends string[] = [Format],
> =
Last<Acc> extends string
? Acc['length'] extends Limit
? Acc[number]
: JoinedUnion<
Format,
Limit,
[...Acc, `${Last<Acc>} ${Format}`]
>
: never
First of all Last
is used to get the type of the last index of a tuple, this will be needed later.
Then this JoinedUnion
type takes:
Format
as the string literal that each term will beLimit
the maximum level of recursion allowed. This tell Typescript when to quit.Acc
is the accumulated tuple of string literal types so far and is used internally to collect results and to know when to stop.
Line by line:
Last<Acc> extends string
solves a type error that I'm not entirely sure why it's an error. But this proves to the type checker that the last item in the tuple is a string type.? Acc['length'] extends Limit
this checks if we are at the limit.? Acc[number]
the limit has been reached, so index the build up tuple bynumber
to get a union of all values.
Then we get to the recursion:
: JoinedUnion<
Format,
Limit,
[...Acc, `${Last<Acc>} ${Format}`]
>
We get here if we are not yet at the desired length. So we call the JoinedUnion
type again with the same format and limit, but append an entry to the tuple we are building up. That tuple is everything we previously build up (...Acc
) then a new item that is a string literal type with the last item in the tuple plus a space and the desired format. This part is what builds the next layer.
Let's test it out:
type TestAB = JoinedUnion<'a' | 'b', 3>
"a" | "b" | "a a" | "a b" | "b a" | "b b" | "a a a" | "a a b" | "a b a" | "a b b" | "b a a" | "b a b" | "b b a" | "b b b"
I think that does what you want.
With your example values, it works as well:
export type Format = `${number}` | `${number}px`;
export type Input = JoinedUnion<Format, 12>
const inputA: Input = '1';
const inputB: Input = '1 2';
const inputC: Input = '1 2 3';
const inputD: Input = '1 2 10px';
const inputE: Input = '10px 1 2 20px';
const inputBad: Input = '10px 1 2 20pxz'; // error
Note: this resolves to a union type with over 8000 members. If you take the length 16 which gives you a member length of about 131,000, that seems to the max Typescript will allow before throwing a:
Expression produces a union type that is too complex to represent.(2590)
And this may make type checking slow.
Whether those tradeoffs are right for your use case is up to you.
Answered By - Alex Wayne
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.