Typescript: Bug: (Any) string is assignable to numeric templated literal type

Created on 14 Sep 2020  ·  9Comments  ·  Source: microsoft/TypeScript

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:

https://github.com/microsoft/TypeScript/pull/40336

Needs Investigation

Most helpful comment

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.

All 9 comments

@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’

See: https://stackoverflow.com/q/63879463/3612643

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:

  1. That Foo just resolves to a string comes as a surprise.

  2. 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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

wmaurer picture wmaurer  ·  3Comments

seanzer picture seanzer  ·  3Comments

MartynasZilinskas picture MartynasZilinskas  ·  3Comments

remojansen picture remojansen  ·  3Comments

Antony-Jones picture Antony-Jones  ·  3Comments