Apart from @T.J. Crowder 's solution, which is perfectly fine, there is another one, which is more focused on type inference.
We can write a function which will infer return type of provided argument. For example:
split('1,2') // [1,2]
In order to achieve it, we need to write an utility type which will parse a string an convert all stringified digits into numerical array.
We need :
- To generate a range of numbers
- Add utility type for convertion a stringified number to numeric digit
- Write a type which will iterate through each character in passed argument , converts it to
number
and add it to the list.
To generate number range, you can check my article and answer:
type MAXIMUM_ALLOWED_BOUNDARY = 100
type ComputeRange<
N extends number,
Result extends Array<unknown> = [],
> =
(Result['length'] extends N
? [...Result, Result['length']][number]
: ComputeRange<N, [...Result, Result['length']]>
)
type ResultRange = ComputeRange<MAXIMUM_ALLOWED_BOUNDARY> // 1 | 2 | 3 .. 100
Then, we need to convert stringified number to numeric digit:
type ToInt<
DigitRange extends number,
Digit extends string
> =
/**
* Every time when you see: [Generic] extends any ...
* It means that author wants to turn on distributivity
* https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types
*/
DigitRange extends any
/**
* After Distributivity was turned on, it means that [true branch] is applied to
* each element of the union separately
*
* SO, here we are checking whether wrapped into string numerical digit is assignable to stringified digit
*/
? `${DigitRange}` extends Digit
/**
* If yes, we return numerical digit, without string wrapper
*/
? DigitRange
: never
: never
type Result = ToInt<1 | 2 | 3 | 4 | 5, '2'> // 2
Now, we need a type which will iterate through each character in string and converts it into numerical digit:
type Inference<
S extends string,
Tuple extends number[] = []
> =
/**
* Check if it is the end of string
*/
S extends ''
/**
* If it is the end - return accumulator generic
*/
? Tuple
/**
* Otherwise infer first and second digits which are separated by comma and rest elements
*/
: S extends `${infer Fst},${infer Scd}${infer Rest}`
/**
* Paste infered digits into accumulator type Tuple and simultaneously convert them into numerical digits
*/
? Inference<
Rest, [...Tuple, IsValid<Fst>, IsValid<Scd>]
>
: never
WHole example:
type MAXIMUM_ALLOWED_BOUNDARY = 100
type ComputeRange<
N extends number,
Result extends Array<unknown> = [],
> =
(Result['length'] extends N
? [...Result, Result['length']][number]
: ComputeRange<N, [...Result, Result['length']]>
)
type ResultRange = ComputeRange<MAXIMUM_ALLOWED_BOUNDARY> // 1 | 2 | 3 .. 100
type ToInt<
DigitRange extends number,
Digit extends string
> =
DigitRange extends any
? `${DigitRange}` extends Digit
? DigitRange
: never
: never
type Result = ToInt<1 | 2 | 3 | 4 | 5, '2'> // 2
type IsValid<Elem extends string> = ToInt<ResultRange, Elem>
type Inference<
S extends string,
Tuple extends number[] = []
> =
S extends ''
? Tuple
: S extends `${infer Fst},${infer Scd}${infer Rest}`
? Inference<
Rest, [...Tuple, IsValid<Fst>, IsValid<Scd>]
>
: never
declare const split: <Str extends `${number},${number}`>(str: Str) => Inference<Str>
split('1,2') // [1,2]
Playground
As you might have noticed, my focus was more on types rather than business logic. Hope it helps to understand how TS type system works.
Btw, since TS 4.8 (nightly version) you can infer digit from template literal without any recursion hacks. See this answer