One way to describe this is by creating a generic utility type called HasSameKeysAs<T, U>
. This type always results in U
, but it will raise an error if the keys are not exactly the same as those in T
:
type HasSameKeysAs<
T, U extends {
[P in keyof T | keyof U]: P extends keyof T ? any : never
}> = U;
The reason this works is because U
is limited to a type that relies on both T
and itself. By iterating over the keys of both T
and U
, we ensure that any key present in T
must be required and can be anything (using any
). On the other hand, any key present in U
but not in T
is constrained to be the impossible never
type, effectively disallowing it (though technically allowing never
, which is unlikely to occur unintentionally).
Let's put it to the test:
type SameKeysWithNeyTypes = HasSameKeysAs<Numbers, {
a: string;
b: Date;
}> // error!
// Property 'f' is missing in type '{ a: string; b: Date; }'
// but required in type '{ a: any; b: any; f: any; }'.
type SameKeysWithNeyyTypes = HasSameKeysAs<Numbers, {
a: string;
b: Date;
c: null;
f: 1;
}> // error!
// Types of property 'c' are incompatible.
type SameKeysWithNeyyyTypes = HasSameKeysAs<Numbers, {
a: string;
b: Date;
f: 1;
}> // okay
It seems to be working correctly. Errors are raised for missing or extra keys, while types with identical keys pass without issues.
Check out the Playground link for this code snippet