If you're unable or unwilling to modify the structure so that UnionType
is based on DerivedType
, or if both UnionType
and DerivedType
cannot be defined in terms of something else, a custom utility type CheckKeys<K, T>
can be created. This utility type will result in T
, but will trigger a compiler error unless all keys in T
precisely match
K</code without any omissions or extras (though there may be some exceptional cases).</p>
<p>One approach to achieve this is as follows:</p>
<pre><code>type CheckKeys<
K extends PropertyKey,
T extends Record<K, any> & Record<Exclude<keyof T, K>, never>
> = T;
In this setup, T
is constrained to ensure it contains all keys from K
(as it extends Record<K, any>
utilizing the Record
utility type). Additionally, through intersection, any keys in T
not present in K
are mandated to have properties of the impossible never
type. Since only never
is assignable to
never</code, and setting property types to <code>never</code explicitly is unlikely, this effectively enforces the desired restriction. Let's put it to the test:</p>
<pre><code>type UnionType = 'prop1' | 'prop2' | 'prop3';
type DerivedType = CheckKeys<UnionType,
{ prop1: string; prop2: number; prop3: boolean; }
>; // success
type ExtraKey = CheckKeys<UnionType,
{ prop1: string, prop2: number, prop3: boolean, prop4: Date } // error!
// Types of property 'prop4' are incompatible.
>
type MissingKey = CheckKeys<UnionType,
{ prop1: string, prop2: number } // error!
// Property 'prop3' is missing.
>
Results look promising. DerivedType
remains unchanged and compiles error-free. However, introducing or removing keys triggers the necessary compiler alerts pinpointing the issue.
Playground link for code