I recently dealt with a tricky TypeScript situation. GraphQL servers have a feature where for any response it returns, it adds a __typename within every object corresponding to its type’s name.

This is what a typical response looks like:

{
  "__typename": "SolarSystem",
  "id": 123,
  "name": "The Solar System",
  "star": {
    "__typename": "Planet",
    "id": 123,
    "name": "Sun",
    "size": 12345,
    "inhabitants": null
  },
  "planets": [
    {
      "__typename": "Planet",
      "id": 123,
      "name": "Earth",
      "size": 12345,
      "inhabitants": [
        {
          "__typename": "LifeForm",
          "id": 123,
          "name": "Human"
        }
      ]
    }
  ]
}

When a GraphQL codegen script runs, this __typename field gets eventually imported into TypeScript world and makes its way into the generated schema:

interface SolarSystem {
  __typename: "SolarSystem";
  id: number;
  name: string;
  star: Planet;
  planets: Planet[];
}

interface Planet {
  __typename: "Planet";
  id: number;
  name: string;
  size: number;
  inhabitants: LifeForm[] | null;
}

interface LifeForm {
  __typename: "LifeForm";
  id: number;
  name: string;
}

Here is a component we have to display a LifeForm entity using the schema types:

interface EntityProps<T> {
  entity: T;
}
function LifeForm({ entity }: EntityProps<LifeForm>) {
  return <>{entity.name}</>;
}

In production, we will typically invoke it using a GraphQL query:

const { data } = useLifeFormQuery();
return <LifeForm entity={data} />;

But what if we want to mock it?

<LifeForm
  entity={{
    __typename: "LifeForm", // This is required, but we will never use it!
    name: "Humans",
  }}
/>

Suddenly, __typename becomes a burden. Our component APIs require something which we will never use (and if we do use it, it would mean coupling our view components to GraphQL’s implementation details – not good!). In this case, getting rid of the __typename requirement is easy: use Omit<T, K>!

interface EntityProps<T> {
  entity: Omit<T, "__typename">;
}

// ...

<LifeForm
  entity={{
    name: "Humans",
  }}
/>;

But what if instead of LifeForm, we need the whole SolarSystem?

interface EntityProps<T> {
  entity: Omit<T, "__typename">;
}
function SolarSystem({ entity }: EntityProps<SolarSystem>) {
  return <>{entity.name}</>;
}
<SolarSystem
  entity={{
    //__typename: "SolarSystem",
    id: 123,
    name: "The Solar System",
    star: {
      __typename: "Planet",
      id: 123,
      inhabitants: null,
      name: "Sun",
      size: 9999,
    },
    planets: [
      {
        __typename: "Planet",
        id: 123,
        name: "Earth",
        size: 12345,
        inhabitants: [
          {
            __typename: "LifeForm",
            id: 123,
            name: "Human",
          },
        ],
      },
    ],
  }}
/>;

The Omit<T, K> allows us to omit the first __typename, but what about the rest? Is it possible to remove all __typename without rewriting the type definition by hand?

Using Mapping Types to Override Definitions

The most practical way of overriding a type definition is by using mapped types.

The TypeScript Handbook: When you don’t want to repeat yourself, sometimes a type needs to be based on another type. Mapped types build on the syntax for index signatures, which are used to declare the types of properties which have not been declared ahead of time.

The most basic mapped type is one that duplicates another type:

interface MyType {
  foo: number;
  bar: boolean;
  baz: "hello" | "world";
}

type MyMappedType = {
  [K in keyof MyType]: MyType[K];
};

// Or more generically...

type Duplicate<T> = {
  [K in keyof T]: T[K];
};

type MyGenericallyMappedType = Duplicate<MyType>;

Playground

Mapped types can extend, restrict, or modify entirely the type they are operating on:

interface MyType {
  foo: number;
  bar: boolean;
  baz: "hello" | "world";
}

type AllowNumbers<T> = {
  [K in keyof T]: T[K] | number;
};

const allowedNumbers: AllowNumbers<MyType> = {
  foo: 5,
  bar: 6,
  baz: 7,
};

type DisallowNumbers<T> = {
  [K in keyof T]: Exclude<T[K], number>;
};

const disallowedNumbers: DisallowNumbers<MyType> = {
  foo: 5, // Error, no numbers allowed
  bar: true,
  baz: "hello",
};

type OnlyNumbers<T> = {
  [K in keyof T]: number;
};

const onlyNumbers: OnlyNumbers<MyType> = {
  foo: 5,
  bar: true, // Error, only numbers allowed
  baz: "hello", // Error, only numbers allowed
};

type DoubleWrapped<T> = {
  [K in keyof T]: { [P in K]: T[K] }; // Nested remapping. P == K
};

const doubleWrapped: DoubleWrapped<MyType> = {
  foo: { foo: 5 },
  bar: { bar: true },
  baz: { baz: "hello" },
};

Playground

The TypeScript Handbook has other interesting examples.

Of particular interest to us is the ability to apply the Omit<T, K> utility to all children of a type:

interface FooType {
  __typename: "First";
  name: string;
  child: {
    __typename: "Second";
    name: string;
    child: {
      __typename: "Third";
      name: string;
    };
  };
}

type OmitFromChildren<T, K extends string> = {
  [P in keyof T]: Omit<T[P], K>;
};

const test: Omit<OmitFromChildren<FooType, "__typename">, "__typename"> = {
  // __typename: "First";
  name: "Top level",
  child: {
    // __typename: "Second",
    name: "Child",
    child: {
      __typename: "Third", // Still Required
      name: "Leaf",
    },
  },
};

Playground

The above allows us to use Omit<T, K> two levels deep. We will later reuse the mapping type technique to apply Omit<T, K> recursively to a complete tree.

Limitations of Omit

Omit<T, K> has a few surprising edge cases that we need to cover before proceeding:

Unions

Under the hood, Omit<T, K> is implemented using Exclude in such a way, that passing a union type to it produces nonsense.

interface FooType {
  __typename: "First";
  name: string;
}
interface FooType2 {
  __typename: "First";
  name2: string;
}

// No static typing is applied
const test: Omit<FooType | FooType2, "__typename"> = {
  name: 123, // Wrong type, but no errors :hmm:
};

Playground

The reason why this behaves the way it does would be a blog post on its own, but suffice to say that the way to fix it is to distribute the union.

Tip: To Distribute means to check every member of a union seperately.

A union can be forcefully distributed by using the extends operator:

// Every type extends `unknown`
type UnionOmit<T, K extends string | number | symbol> = T extends unknown
  ? Omit<T, K>
  : never;

As a sidenote, UnionOmit turns out to be simply a more flexible version of Omit<T, K>. I do not see any value in using Omit<T, K> by itself anymore.

Once the types are distributed, type checking is restored:

interface FooType {
  __typename: "First";
  name: string;
}
interface FooType2 {
  __typename: "First";
  name2: string;
}

type UnionOmit<T, K extends string | number | symbol> = T extends unknown
  ? Omit<T, K>
  : never;

const test: UnionOmit<FooType | FooType2, "__typename"> = {
  name: 123, // TypeError: type checking is back!
};

Playground

Nullable Types

You may think that a nullable type, defined as T | null is just a regular union, but it sometimes behaves differently than other types. I’ll admit that I don’t have this part fully figured out, but my working theory is that null types are sticky and do not distribute like other types.

type UnionOmit<T, K extends string | number | symbol> = T extends unknown
  ? Omit<T, K>
  : never;

interface FooType {
  __typename: "First";
  name: string;
}

// No static typing is applied
const test: Omit<FooType | null, "__typename"> = {
  name: 123, // Wrong type, but no errors :hmm:
};

// No static typing is applied... AGAIN!
const test2: UnionOmit<FooType | null, "__typename"> = {
  name: 123, // Wrong type, but no errors :hmm:
};

Playground

We can forcefully distribute them using a different technique:

type UnionOmit<T, K extends string | number | symbol> = T extends unknown
  ? Omit<T, K>
  : never;
type NullUnionOmit<T, K extends string | number | symbol> = null extends T
  ? UnionOmit<NonNullable<T>, K> | null
  : UnionOmit<T, K>;

interface FooType {
  __typename: "First";
  name: string;
}

const test: NullUnionOmit<FooType | null, "__typename"> = {
  name: 123, // TypeError: type checking is back!
};
const test2: NullUnionOmit<FooType | null, "__typename"> = null; // Nulls are legal

Playground

Functions

A function is a special kind of object: it has the property of being callable. TypeScript considers this property fragile enough to strip it from any function definition that has been touched by Omit<T, K>:

type OmittedFunction = Omit<() => true, "key">;
let func: OmittedFunction = () => true;
func(); // This expression is not callable.
//   Type 'OmittedFunction' has no call signatures.

Playground

One solution to this is to specifically avoid touching functions:

type FunctionSafeOmit<T, K extends string> = T extends Function
  ? T
  : Omit<T, K>;
type OmittedFunction = FunctionSafeOmit<() => true, "key">;
let func: OmittedFunction = () => true;
func();

Playground

There is nothing wrong with this method technically, but it does not inspire confidence. What if functions are not the only type that breaks in this way? While it might be the case today, new special types are added to JavaScript (Classes, Promises, Symbols, Proxies, etc) every few years. It might not be true tomorrow.

This brings us to the second, preferred solution. Instead of blacklisting every type individually, we can selectively apply Omit<T, K> to the types which have the property we wish to omit:

type SafeOmit<T, K extends string> = T extends { [P in K]: any }
  ? Omit<T, K>
  : T;
type OmittedFunction = SafeOmit<() => true, "key">;
let func: OmittedFunction = () => true;
func();

Playground

Writing Recursive Generics

Time to breathe a breath of fresh air: The difficult parts are behind us. Recursive Generics in TypeScript may be tricky at times, but they don’t have gotchas. As long as you do not attempt to write an infinitely deep expression, it should be fine.

A recursive mapping type looks like this:

type Recursive<T> = {
  [P in keyof T]: Recursive<T[P]>;
} & { changed: true };

If we want to recurse conditionally, we need to only recurse when appropriate:

// Do not recurse on specific structure
type Recursive<T> = {
  [P in keyof T]: T[P] extends { value: number } ? T[P] : Recursive<T[P]>;
} & { changed: true };

The above will stop recursing on any branch if it hits a match. If our goal is to recurse on the whole type while applying an Omit<T, K>, the recursion must always happen, but the application becomes conditional:

type Recursive<T> = T extends { value: number }
  ? { [P in keyof T]: Recursive<T[P]> }
  : {
      [P in keyof T]: Recursive<T[P]>;
    } & { changed: true };

let test: Recursive<{ foo: { bar: { value: 3 }; baz: { value: true } } }> =
  null as any;

test.foo.bar.changed; // Property 'changed' does not exist on type
test.foo.baz.changed; // Property 'changed' exists

Playground

The duplicated portion of the definition can be extracted into a helper type:

type RecursiveHelper<T> = { [P in keyof T]: Recursive<T[P]> };
type Recursive<T> = T extends { value: number }
  ? RecursiveHelper<T>
  : RecursiveHelper<T> & { changed: true };

let test: Recursive<{ foo: { bar: { value: 3 }; baz: { value: true } } }> =
  null as any;

test.foo.bar.changed; // Property 'changed' does not exist on type
test.foo.baz.changed; // Property 'changed' exists

Playground

Putting It All Together

type UnionOmit<T, K extends string | number | symbol> = T extends unknown
  ? Omit<T, K>
  : never;
type NullUnionOmit<T, K extends string | number | symbol> = null extends T
  ? UnionOmit<NonNullable<T>, K>
  : UnionOmit<T, K>;
type RecursiveOmitHelper<T, K extends string | number | symbol> = {
  [P in keyof T]: RecursiveOmit<T[P], K>;
};
type RecursiveOmit<T, K extends string | number | symbol> = T extends {
  [P in K]: any;
}
  ? NullUnionOmit<RecursiveOmitHelper<T, K>, K>
  : RecursiveOmitHelper<T, K>;

const cleanSolarSystem: RecursiveOmit<SolarSystem, "__typename"> = {
  //__typename: "SolarSystem",
  id: 123,
  name: "The Solar System",
  star: {
    //__typename: "Planet",
    id: 123,
    inhabitants: null,
    name: "Sun",
    size: 9999,
  },
  planets: [
    {
      //__typename: "Planet",
      id: 123,
      name: "Earth",
      size: 12345,
      inhabitants: [
        {
          //__typename: "LifeForm",
          id: 123,
          name: "Human",
        },
      ],
    },
  ],
};

Playground

Bonus Feature

As a cherry on top, not only does T accept unions, but so does K:

interface Foo {
  a: 3;
  b: 4;
  c: 5;
}
type Bar = RecursiveOmit<Foo, "a" | "b">;
let x: Bar = {
  c: 5,
};