Search code examples
typescripttypesasserttypeofkeyof

Typescript check if type A === type B | type C


In one file I have something like this:

export const _all = {
  a: '',
  b: '',
  c: '',
  d: '',
  e: '',
  f: '',
}
type AllKeysType = typeof _all;
export type AllKey = keyof AllKeysType;

In another file I have something like this:

export const _keep = {
  a: '',
  b: '',
  d: '',
  e: '',
}
type KeepKeysType = typeof _keep;
export type KeepKey = keyof KeepKeysType;

export const _ignore = {
  c: '',
  f: '',
}
type IgnoreKeysType = typeof _ignore;
export type IgnoreKey = keyof IgnoreKeysType;

How can I use Typescript to assert that the keys defined in _all ALWAYS is equal to the union of _keep and _ignore. In other words, AllKey should always be equal to KeepKey | IgnoreKey.

I want the Typescript compiler to give me an error if a developer updates _all by adding in a new value (say z) but forgets to add z into either _keep or _ignore.


Solution

  • This is possible by defining a conditional type that accepts two types and resolves to true when the input types are equal or false otherwise. Then write some code that will throw a compile error when that type is not true.

    When either of the types change you'll get a compile error which will ensure you remember to update whichever type is out of sync. This is especially useful when you want to be notified about changes to a type in a different library.

    For example:

    type IsExact<T, U> = [T] extends [U] ? [U] extends [T] ? true : false : false;
    function assert<T extends true | false>(expectTrue: T) {}
    
    // this will throw a compile error when the two types get out of sync
    assert<IsExact<AllKey, KeepKey | IgnoreKey>>(true);
    

    More robust code is a little longer (ex. handling the any type), but it's rolled up in my library here.

    import { assert, IsExact } from "conditional-type-checks";
    
    // define or import AllKey, KeepKey, IgnoreKey
    
    assert<IsExact<AllKey, KeepKey | IgnoreKey>>(true);
    

    Another Option

    Another not so nice way of doing this is to create two objects of the two types and assign them to each other.

    () => {
      let allKeys: AllKey;
      let otherKeys: KeepKey | IgnoreKey;
    
      // do this in lambdas to prevent the first assignment from changing
      // the type of the variable being assigned to
      () => allKeys = otherKeys;
      () => otherKeys = allKeys;
    };