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 implement ExhaustiveRecord
directly.
Instead the best we can do is write a generic type ExhaustivePersonKeyRecord<T>
that acts as a constraint on T
so that T extends ExhaustivePersonKeyRecord<T>
if and only if T
has a property value for every key of Person
.
Once we have that, we'd write a generic helper identity function that uses such a type, like this:
function exhaustivePersonKeyRecord<T extends Record<keyof T, keyof Person>>(
t: ExhaustivePersonKeyRecord<T>
) { return t }
That will make it so that exhaustivePersonKeyRecord({a: "Name", b: "Age"})
will succeed but exhaustivePersonKeyRecord({a: "Name"})
will fail.
All we have to do is define an appropriate ExhaustivePersonKeyRecord<T>
type. Here's one possible way to do it:
declare const __some_property__: unique symbol;
type ExhaustivePersonKeyRecord<T> = T & (keyof Person extends T[keyof T] ? unknown :
{ [__some_property__]: Exclude<keyof Person, T[keyof T]> }
)
That's T
intersected with a conditional type that compares keyof Person
to T[keyof T]
. The type T[keyof T]
is the union of all property types included in T
(as discussed in Is there a `valueof` similar to `keyof` in TypeScript? ). If keyof Person extends T[keyof T]
it means that T[keyof T]
is wider than keyof Person
and thus contains every member. That's what you want.
If that check is true and you used all the keys of Person
in the properties of T
, then the conditional type evaluates to the unknown
type and T & unknown
is just T
. And T 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 type Exclude<keyof Person, T[keyof T]>
, using the Exclude
utility type to determine the missing keys. If T
is, say, {a: "Name"}
, then Exclude<keyof Person, T[keyof T]>
will be "Age"
, and the whole type will evaluate to {a: "Name"} & {[__some_property__]: "Age"}
, and so T extends ExhaustivePersonKeyRecord<T>
will be false, and it will fail.
Let's test it out:
class Some {
public StringToKey = exhaustivePersonKeyRecord({
"N": "Name",
"A": "Age",
}); // okay
}
That works until we augment Person
:
class Person {
public Name: string = '';
public Age: number = 0;
public Height: number = 0; // <-- do this
}
Then we get the desired error:
class Some {
public StringToKey = exhaustivePersonKeyRecord({
"N": "Name",
"A": "Age"
}); // error
}
// Property '[__some_property__]' is missing in type '{ N: "Name"; A: "Age"; }'
// but required in type '{ [__some_property__]: "Height"; }'.
which hopefully gives us enough information to fix it:
class Some {
public StringToKey = exhaustivePersonKeyRecord({
"N": "Name",
"A": "Age",
"H": "Height"
}); // okay
}
Looks good!