I make a simple example just to show a problem. There it is
class Person{
public Name: string = '';
public Age: number = 0;
}
class Some {
public StringToKey: Record<string, keyof Person> = {
"N": "Name",
"A": "Age"
}
public KeyToVal: {[Key in keyof Person]: (value:string)=>Person[Key]} = {
Name: (value: string) => {return value;},
Age: (value: string) => {return Number(value);}
}
}
All looks good and compile. If i add new property to Person, i want to get 2 errors: for StringToKey and KeyToVal, but i only have error in KeyToVal situation.
So, for StringToKey declaration i can't be sure that i will get error if something will be changed in Person class, which is bad for me.
Is there any magic in type declaration i could do to get error in StringToKey declaration when Person class has been changed?
TypeScript really doesn't have a way to represent an "exhaustive" type that ensures that each member of some union is known to be present as a property value. So no specific type like
type ExhaustivePersonKeyRecord = ExhaustiveRecord<string, keyof T>exists, since there's no way to implementExhaustiveRecorddirectly.Instead the best we can do is write a generic type
ExhaustivePersonKeyRecord<T>that acts as a constraint onTso thatT extends ExhaustivePersonKeyRecord<T>if and only ifThas a property value for every key ofPerson.Once we have that, we'd write a generic helper identity function that uses such a type, like this:
That will make it so that
exhaustivePersonKeyRecord({a: "Name", b: "Age"})will succeed butexhaustivePersonKeyRecord({a: "Name"})will fail.All we have to do is define an appropriate
ExhaustivePersonKeyRecord<T>type. Here's one possible way to do it:That's
Tintersected with a conditional type that compareskeyof PersontoT[keyof T]. The typeT[keyof T]is the union of all property types included inT(as discussed in Is there a `valueof` similar to `keyof` in TypeScript? ). Ifkeyof Person extends T[keyof T]it means thatT[keyof T]is wider thankeyof Personand thus contains every member. That's what you want.If that check is true and you used all the keys of
Personin the properties ofT, then the conditional type evaluates to theunknowntype andT & unknownis justT. AndT extends T, so it will succeed.If that check is false, and you missed some key or keys of
Person, then the conditional type evaluates to{ [__some_property__]: Exclude<keyof Person, T[keyof T]> }. This is an object type with the key of type__some_property__(a fake symbol type I declared just to have some "key" to say we missed) and a value of typeExclude<keyof Person, T[keyof T]>, using theExcludeutility type to determine the missing keys. IfTis, say,{a: "Name"}, thenExclude<keyof Person, T[keyof T]>will be"Age", and the whole type will evaluate to{a: "Name"} & {[__some_property__]: "Age"}, and soT extends ExhaustivePersonKeyRecord<T>will be false, and it will fail.Let's test it out:
That works until we augment
Person:Then we get the desired error:
which hopefully gives us enough information to fix it:
Looks good!
Playground link to code