Search code examples
typescriptreadonlymapped-types

Making a Mapped Type in TypeScript where readonly keys can be set to a specific value


I'd like to create a generic mapped type in TypeScript with the following concepts:

  1. Allows any writable key from the base type to be set to a value (same type as in the base type) or a pre-defined flag
  2. Allows readonly keys to be set ONLY to the pre-defined flag.

Here is a non-working example of the concept:

type KindOfMutable1<T> = {
    -readonly[P in keyof T]?: "FLAG";
} | {  // THIS DOES NOT WORK
    [P in keyof T]?: T[P] | "FLAG"
};

interface thingy {
    x: number;
    readonly v: number;
}
const thing1: KindOfMutable1<thingy> = {x: 1};
thing1.v = "FLAG";
//     ^ ERROR HERE: Cannot assign to 'v' because it is a read-only property

Another way to think about my desired solution would look something like this:

// pseudo code of a concept:
type KindOfMutable2<T> = {
    [P in keyof T]?: /* is T[P] readonly */ ? "FLAG" : T[P] | "FLAG"
};

Is there any way to do this?


Solution

  • Detecting readonly properties vs mutable properties is tricky, because object types that differ only in their "read-onliness" are considered mutually assignable. A variable of type {a: string} will accept a value of type {readonly a: string} and vice versa. See microsoft/TypeScript#13347 for more information.

    It is possible, but only by using a technique shown in this answer where we get the compiler to tell us whether it considers two types "identical" as opposed to just mutually assignable:

    type IfEquals<X, Y, A = X, B = never> =
        (<T>() => T extends X ? 1 : 2) extends
        (<T>() => T extends Y ? 1 : 2) ? A : B;
    
    type MutableProps<T> = {
        [P in keyof T]-?: IfEquals<
          { [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P>
    }[keyof T];
    

    The MutableProps<T> type gives you the non-readonly keys of T, by comparing whether an explicitly non-readonly version of the property is "identical" to the original version:

    type MutablePropsOfThingy = MutableProps<Thingy>;
    // type MutablePropsOfThingy = "x"
    

    And so you can write KindOfMutable<T> specifically in terms of MutableProps<T>:

    type KindOfMutable<T> = {
        -readonly [P in keyof T]?: "FLAG" | (P extends MutableProps<T> ? T[P] : never)
    }
    

    Resulting in:

    type KindOfMutableThingy = KindOfMutable<Thingy>;
    /* type KindOfMutable1Thingy = {
        x?: number | "FLAG" | undefined;
        v?: "FLAG" | undefined;
    } */
    

    which works how you want:

    const thing1: KindOfMutable<Thingy> = { x: 1 }; // okay
    thing1.v = "FLAG"; // okay
    

    Playground link to code