Typescript Template String Types as Discriminants

Improved template literal types with Typescript 4.5

What is "Narrowing" in TS?

Before we take a look at the enhancement of literal types in Typescript 4.5 and later, I just quickly want to recap what “Narrowing” in this context actually means. Basically you can narrow down a type in Typescript by checking the entity's properties. If a property exists on the entity that’s only available for a certain type, then Typescript understands this and can therefore provide the correct types.

The following code example demonstrates what I mean by that.

type Addition = {
    sum: number;
}

type Subtraction = {
    result: number;
}

function calculate(action: Addition | Subtraction) {
    if ('sum' in action) {
      // Simple example of using properties
      // of an entity to narrow down its
      // actual type.
      //
      // 'action' is at this point of type
      // 'Addition'. Nice!
      const addition = action;
    }
}

Narrowing for Template String Types

Starting with Typescript version 4.5, this pattern can also be applied for template literal types, also known as those special strings with backticks. The pattern matching is becoming really powerful in the language.

Of course, for narrowing to become usable, all types in the comparison have to share the same property key. Otherwise, you can simple use the common narrowing like above.

The following code example shows how Template Strings can use Narrowing:

type Addition = {
    variant: `${string}-addition`;
    sum: number;
}

type Subtraction = {
    variant: `${string}-subtraction`;
    result: number;
}

function process(action: Addition | Subtraction) {
    if (action.variant === "simple-addition") {
        // Stupid simple example, but you get the idea:
        // We used 'simple-addition', and the pattern 
        // matching by TS understood that '...-addition'
        // can be discriminated to 'Addition'.
        const addition = action;
    }
}
//
// Example that DOES NOT work.
//
// This demo illustrates that the
// discriminante has to be a string,
// else the pattern matching worn't work.

type Addition = {
    variant: `${string}-arithemtic`;
    sum: number;
}

type Subtraction = {
    variant: `${number}-arithemtic`;
    result: number;
}

function process(action: Addition | Subtraction) {
    if (action.variant === "123-arithemtic") {
        // Still 'Addion | Subtraction',
        // as we can only compare against
        // a string '123-arithmetic', which
        // is of course the same for both.
        const addition = action;
    }
}

And now you know about narrowing down template literals in Typescript! This might be a feature you won’t use very often, but when it’s appropriate, I think it can greatly reduce code complexity and keep your codebase a little cleaner than without the feature.

Suggestions

Related

Addendum

Languages