Omitting all properties in an object which are `undefined`

Viewed 263

I have a type like this:

  {
    a: number,
    b: number,
    c: undefined
  }

And I want to map it to a type like this:

{
  a: number,
  b: number
}

i.e. I want to omit all keys where the type of the value is undefined by definition. The type comes from a generic so I obviously cannot do it manually.

I know I can map a type to another type with a mapper, so I tried mapping undefined to never:

type TryOmitUndefined<K> = {
  [k in keyof K]: K[k] extends undefined ? never : K[k]
}

But this doesn't work at all.

Is it possible in current TypeScript to omit keys from a type based on their value?

Bonus question

Can I map this

{
  a: number,
  b: number | undefined,
  c: undefined
}

to this?

{
  a: number,
  b?: number
}
2 Answers

It's probably easiest to do this in two steps. First get the undefined keys:

type UndefinedKeys<T> = {
    [K in keyof T]: T[K] extends undefined ? K : never
}[keyof T]

And now Omit those keys:

type WithoutUndefined<T> = Omit<T, UndefinedKeys<T>>

Then it should work as expected:

type Bar = WithoutUndefined<Foo>
const bar: Bar = { a: 1, b: 2 }

Playground


The UndefinedKeys<T> type works like this:

The [] after a type says to give all possible value types as a union that match those key types.

type StringArray = string[]
type A = StringArray[number] // string

interface Abc { a: string, b: number, c: boolean }
type B = Abc['a' | 'b' | 'c'] // string | number | 'boolean'

type AbcKeys = keyof Abc // 'a' | 'b' | 'c'
type C = Abc[keyof Abc] // string | number | 'boolean'

So given that, we map over a type to create a new object type where: (psuedocode)

type KeyOrNever = { [originalKey]: originalKey or never }

Or in the case of your example:

type KeyOrNever = { a: "a", b: "b", c: never }

So each key either has a value of its own name, or a value of never. Now we can get the values of all properties as a union by indexing the type by its own keys. This would yield:

type KeysAsUnion = "a" | "b" | never

never is discarded from unions because it serves no purpose. You cannot be possibly one thing, or possibly another thing that can never exist, so it's simply ignored. And this the union simplifies to:

type KeysAsUnion = "a" | "b"

Found a great solution from @ahejlsberg that got me most of the way:

// https://github.com/microsoft/TypeScript/pull/21919#issuecomment-365491689

type OptionalPropNames<T> = { [P in keyof T]: undefined extends T[P] ? P : never }[keyof T];
type RequiredPropNames<T> = { [P in keyof T]: undefined extends T[P] ? never : P }[keyof T];

type OptionalProps<T> = { [P in OptionalPropNames<T>]: T[P] };
type RequiredProps<T> = { [P in RequiredPropNames<T>]: T[P] };

type Foo = {
    a: string;
    b: number;
    c?: string;
    d?: number;
}

type T1 = RequiredProps<Foo>;  // { a: string, b: number }
type T2 = OptionalProps<Foo>;  // { c?: string | undefined, d?: number | undefined }

This shows how to omit keys based on their value (not sure why my example solution does not work—would appreciate any insights), but it doesn't handle the specific case where a type is explicitly set to undefined.

I extended it with this:

type TrueUndefinedPropNames<T> = { [P in keyof T]: T[P] extends undefined ? P : never  }[keyof T];
type TrueUndefinedProps<T> = { [P in TrueUndefinedPropNames<T>]: T[P] };

Then the solution becomes:

type WhatIWant<T> = Omit<
  T,
  TrueUndefinedPropNames<T>
>

Or, if I also want to turn required props of the form K | undefined into optional props:

type WhatIWant2<T> = RequiredProps<T> & Omit<
  Partial<OptionalProps<T>>,
  TrueUndefinedPropNames<T>
>

Demo:

type TestType = {
  a: undefined;
  b: number;
  c: number | undefined;
}

const x: TestType = { b: 3 } // Fails
const y: WhatIWant<TestType> = { b: 3, c: undefined } // Succeeds
const z: WhatIWant2<TestType> = { b: 3 } // Succeeds!
Related