angulartypescripttype-narrowing

Is it possible to modify a Typescript type alias dynamically?


I am working on an Angular library that will serve as an API client. The issue I'm running into is that some of the consuming applications are using an HttpInterceptor to automatically convert date strings into Javascript Date objects while others just use the string value passed in the response.

I currently have these properties typed as string | Date but I would really like a way to have the union type narrowed to either string or Date based on which application is consuming the library.

I first tried declaring a type alias type DateType = string | Date and hoped I could override it using a declare global block in the consuming application but that didn't work.

I also considered defining an interface interface DateType extends String and then overriding that in the consuming application but I abandoned that route as I don't want to have the consuming applications have to manage the conversion between the String object and primitive.


Solution

  • TypeScript doesn't support arbitrary modifications to existing types. In particular you can't modify type aliases. But TypeScript does support certain type modifications through declaration merging, which you can use (or abuse) to get the behavior you're looking for.


    Essentially you can "re-open" an interface declaration and add properties to it. If the library's types are defined like this:

    interface DateTypeConfig {
        [x: string]: string | Date
    }
    type DateType = DateTypeConfig["type"]
    
    declare function acceptDate(date: DateType): void;
    declare function produceDate(): DateType;
    declare function modifyDate(date: DateType): DateType;
    

    Then DateType is an indexed access into the type property of the DateTypeConfig interface. Now the DateTypeConfig interface doesn't currently have a specific type property, but it does have a string index signature of type string | Date, so to start with, DateType is string | Date.

    So by default, a consuming app would see this union.

    const d = produceDate();
    // const d: string | Date
    const m = modifyDate(d);
    // const m: string | Date
    acceptDate(m);
    

    But the consuming app could re-open the DateTypeConfig interface and add an explicit type property whose type is some subtype of string | Date. (If this is in a module or your library is in a module you might need to use global augmentation or module augmentation to get the right namespace.)

    For example:

    interface DateTypeConfig {
        type: Date;
    }
    

    And then automatically, the DateType type alias becomes Date instead of string | Date:

    const d = produceDate();
    // const d: Date
    const m = modifyDate(d);
    // const m: Date
    acceptDate(m);
    

    Or instead the app could do this:

    interface DateTypeConfig {
        type: string;
    }
    

    and then DateType would be string:

    const d = produceDate();
    // const d: string
    const m = modifyDate(d);
    // const m: string
    acceptDate(m);
    

    Playground link to code