Skip to content

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

Description

@derolf

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:

#40336

Activity

  1. changed the title [-]Bug: (Any string) is assignable to templated literal type[/-] [+]Bug: (Any string) is assignable to numeric templated literal type[/+] on Sep 14, 2020
  2. changed the title [-]Bug: (Any string) is assignable to numeric templated literal type[/-] [+]Bug: (Any) string is assignable to numeric templated literal type[/+] on Sep 14, 2020
  3. added
    Working as IntendedThe behavior described is the intended behavior; this is not a bug
    Needs InvestigationThis issue needs a team member to investigate its status.
    and removed
    Working as IntendedThe behavior described is the intended behavior; this is not a bug
    on Sep 14, 2020
  4. RyanCavanaugh commented on Sep 14, 2020

    @RyanCavanaugh
    Member

    Anders Hejlsberg (@ahejlsberg) how is this supposed to work?

  5. tpict commented on Sep 14, 2020

    @tpict

    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.

  6. derolf commented on Sep 14, 2020

    @derolf
    Author

    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

  7. ahejlsberg commented on Sep 14, 2020

    @ahejlsberg
    Member

    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.

  8. tpict commented on Sep 14, 2020

    @tpict

    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.

  9. ahejlsberg commented on Sep 14, 2020

    @ahejlsberg
    Member

    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.

  10. tpict commented on Sep 14, 2020

    @tpict

    That’s a neat trick, thanks!

  11. derolf commented on Sep 15, 2020

    @derolf
    Author

    Anders Hejlsberg (@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.

  12. derolf commented on Sep 24, 2020

    @derolf
    Author

    Fixed by #40598

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Needs InvestigationThis issue needs a team member to investigate its status.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions