1

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?

FTLPhysicsGuy
  • 1,035
  • 1
  • 11
  • 23
  • Does [this approach](https://tsplay.dev/mbBAEm) meet your needs? If so I could write up an answer explaining; if not, what am I missing? – jcalz Jun 10 '22 at 03:19
  • @jcalz That looks like it would meet my needs. Please do write it up in an answer. Thanks! – FTLPhysicsGuy Jun 10 '22 at 20:46

1 Answers1

2

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

jcalz
  • 264,269
  • 27
  • 359
  • 360