Issue
I'd like to create a generic mapped type in TypeScript with the following concepts:
- 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
- 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
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.