typescripttype-inferenceconditional-types

How does TypeScript infer in this conditional type?


How does TypeScript infer U in the following code?

type ExtractPrefix<T> = T extends `${infer U}abc` ? U : never;
const mystring = "helloabc"
type Result = ExtractPrefix<typeof mystring>; // Result is 'hello'

Why does ${infer U}abc extract only "hello"?

Also, if I change the pattern to ${infer U}c, the result becomes "helloab". How does TypeScript determine where to stop inferring U?

I’m struggling to understand how infer U works. Could someone clarify?


Solution

  • TypeScript checks to see if the type parameter, T, extends the template literal type. Here is a simpler example without infer:

    type CheckPrefix<T> = T extends `${string}abc` ? true : never;
    

    Here, you are telling Typescript to check if T ends with abc. You can use this pattern to check any substring. For example, you could use the template literal type ${string}abc${string}xyz to check if a string literal type contains abc and ends with xyz. Note that an empty string ("") is valid type, so "abcxyz" would still satisfy ${string}abc${string}xyz.

    Inferance

    Typescript checks whether T can be split into some string U followed by the literal "abc". Since "helloabc" ends with "abc", Typescript gives you the true branch on the conditional— which is U, the rest of T. This is why "hello" is extracted from ${infer U}abc.

    Typescript checks that the literal part(s) of the type (e.g. "abc" or "c") exactly matches T. The compiler "stops" inferring U when the next literal part of the type occurs. When you change the literal part of your type to "c", the "rest" of the type is "helloab". This is then inferred as the type U, which is your result in the true branch.

    It's also worth noting that you can use extends inside the infer. For example:

    type TLD = 'gov' | 'edu' | 'com' | 'net' | 'dev';
    
    type Domain = `${string}.${TLD}`;
    
    type DomainForEmail<T> = T extends `${string}@${infer TDomain extends Domain}` ? TDomain : never;
    
    type _ = DomainForEmail<'john@example.com'>; // "example.com"
    

    Template Literal Types has a lot of information on this and is worth a read.