In TypeScript 4.1 which dev version is already avalible through npm there are support for Recursive Conditional Types and Template literal types what creates some really interesting opportunities
Let's assume we have following type
// type is '0123456';
const actualString = '0123456';
Split a string by characters into new array, but type of array elements should be preserved
// Unfortunately, type is string[]
const chars1 = actualString.split('');
// Throws error: string[] is not assignable ['0', '1', '2', '3', '4', '5', '6']
const chars2: ['0', '1', '2', '3', '4', '5', '6'] = actualString.split('');
type StringToChars<BASE extends string> = BASE extends `${infer _}`
? BASE extends `${infer FIRST_CHAR}${infer REST}` // BASE is inferable
? [FIRST_CHAR, ...StringToChars<REST>] // BASE has at least one character
: [] // BASE is empty string
: string[]; // BASE is simple string
// type is ['0', '1', '2', '3', '4', '5', '6']
type Chars = StringToChars<'0123456'>;
This solution works fine for strings lesser than 14 chars.
// Throws: Type instantiation is excessively deep and possibly infinite. (ts2589)
type LargeCharsArray = StringToChars<'0123456789 01234'>
Obviously it runs into typescript types recursion limit, after checking on 14th char it leaves us with [<first 14 characters>, ...any[]]
.
This recursive type call looks really bad, so I was wondering, is there more reliable way to transform string type into array of chars type?
There are rather shallow recursion limits, and the pull request implementing template literal types, microsoft/TypeScript#40336 mentions that this is a pain point when you try to pull apart strings one character at a time. From this comment by the implementer:
Note that these types pretty quickly run afoul of the recursion depth limiter. That's an orthogonal issue I'm still thinking about.
So for now I don't think there's a solution that's going to work for strings of arbitrary length.
That said, you can play around with the limit by breaking off larger chunks at a time so that you don't have to recurse as much. There's probably some optimal way of doing this, but I just used trial and error to find something that increases the longest splittable string from ~14 to ~80 characters:
type StringToChars<T extends string> =
string extends T ? string[] :
T extends `${infer C0}${infer C1}${infer C2}${infer C3}${infer C4}${infer C5}${infer R}` ? [C0, C1, C2, C3, C4, C5, ...StringToChars<R>] :
T extends `${infer C0}${infer C1}${infer C2}${infer C3}${infer R}` ? [C0, C1, C2, C3, ...StringToChars<R>] :
T extends `${infer C0}${infer R}` ? [C0, ...StringToChars<R>] : []
type WorksWith80 = StringToChars<'12345678911234567892123456789312345678941234567895123456789612345678971234567898'>;
type Len80 = WorksWith80['length']; // 80
type BreaksWith81 = StringToChars<'123456789112345678921234567893123456789412345678951234567896123456789712345678981'>;
type Len81 = BreaksWith81['length']; // number
You can see that instead of just grabbing one character, we try to grab six. If that fails, we go for four, and then fall back to one.
You might be able to push the limits farther by playing around with that, but I don't really see the point: this algorithm is ugly and is seemingly unmotivated without a bunch of comments on recursion limits; TS isn't quite ready for this stuff at the moment. Hopefully @ahejlsberg will make this better sometime and I can come back here with better news.