TypeScript Version:* 4.1.0-dev.20200914
Search Terms:
Templated literal types
Code
type Foo<T extends number> = `${T}`;
const foo : Foo<number> = "bar"; <-- should fail to compile
Expected behavior:
It should fail to compile since "bar" should not be assignable to Foo<number>.
Actual behavior:
Compiles.
Playground Link:
https://www.typescriptlang.org/play?ts=4.1.0-dev.20200914#code/C4TwDgpgBAYg9nAPAFShAHsCA7AJgZymwFcBbAIwgCcA+KAXigAMASAb2QF8mBuAKADGcbPmBQAhlABcsBIhIVqdRgCJy4qip5A
Related Issues:
@ahejlsberg how is this supposed to work?
Isn’t this intended behaviour?
Any one of the types any, string, number, boolean, or bigint in a placeholder causes the template literal to resolve to type string.
If it works as intended, how can I define a string-type that contains only numbers and has a certain suffix?
Example: ‘42px’
This is working as intended, but could be a suggestion. The instantiation Foo<number> resolves to `${number}` which then resolves to string (we do the same when a placeholder is instantiated to string or bigint).
We could consider keeping templates such as `${number}` or `start-${string}-end` around and introduce assignability rules similar to what we do in type inference. I had some of that in place at one time, but was concerned it leads us down the slippery slope towards full on regular expression types. But we're sort of there already because of type inference, so maybe.
I wrote this for my own project, there’s certainly room for improvement though.
This is bound to be _such_ a common application of template literals that I do think it warrants something more ergonomic and performant.
Here's how I'd write it:
type MatchDigit<D extends string> =
D extends '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ? D : never;
type MatchInteger<S extends string, T = S> =
S extends `${MatchDigit<infer _D>}${infer R}` ? R extends '' ? T : MatchInteger<R, T> : never;
type MatchDecimal<S extends string> =
S extends `${'' | '+' | '-'}${MatchInteger<infer _I>}` ? S :
S extends `${'' | '+' | '-'}${MatchInteger<infer _I>}.${MatchInteger<infer _F>}` ? S :
never;
type MatchExtent<S extends string> =
S extends `${MatchDecimal<infer _>}${'px' | 'pt'}` ? S : never;
declare function takeExtent<S extends string>(ex: MatchExtent<S>): void;
takeExtent('100px');
takeExtent('-1px');
takeExtent('9.5pt');
Note the trick of applying the matching productions to the infer X placeholders. This works as long as each production resolves to its own type argument in one of its branches (because when inferring to a conditional type we infer to each of the branches). Also note that the validation happens during type inference, not during relationship checking. That means it isn't possible to declare a type Extent and have validation occur in assignments.
That’s a neat trick, thanks!
@ahejlsberg thanks for the proposal, but two remarks:
That Foo
Your construct above still looks to convoluted for such a common use case (“5px”, “2.5em”) and having a short straightforward way to declare such a type would be great.
EDIT: and for sure it should also work at assignment stage, so that we can define interfaces that have fields with templates types.
Most helpful comment
This is working as intended, but could be a suggestion. The instantiation
Foo<number>resolves to`${number}`which then resolves tostring(we do the same when a placeholder is instantiated tostringorbigint).We could consider keeping templates such as
`${number}`or`start-${string}-end`around and introduce assignability rules similar to what we do in type inference. I had some of that in place at one time, but was concerned it leads us down the slippery slope towards full on regular expression types. But we're sort of there already because of type inference, so maybe.