I'd like to create a generic mapped type in TypeScript with the following concepts:
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?
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