beraliv

Extract object type with optional fields in TypeScript

Example of GetOptional use
1type GetOptional<T> = any; // implementation
2
3type cases = [
4 Expect<Equal<GetOptional<{ foo: number; bar?: string }>, { bar?: string }>>,
5 Expect<Equal<GetOptional<{ a: undefined; b?: undefined }>, { b?: undefined }>>
6];

Today we discuss GetOptional

Here we consider multiple approaches including new possibilities from TypeScript 4.1

Let's have a look ⤵️

Pick only optional keys

First things first, we split the solution into 2 parts.

Use OptionalKeys to get optional keys
1type OptionalKeys<T> = keyof T;
2
3type GetOptional<T> = Pick<T, OptionalKeys<T>>;

Let's implement OptionalKeys properly as now it returns all the keys from the object type.

If we have an optional key, we can skip the definition of it in the object. It means that given the object with only one optional key, it's allowed to assign empty object to it.

Meaning of optional key
1type WithOptional = { a?: string };
2type WithRequired = { a: string };
3
4// ✅ allowed
5let obj1: WithOptional = {};
6// @ts-expect-error ❌ Property 'a' is missing in type '{}' but required in type 'WithRequired'
7let obj2: WithRequired = {};

Knowing that, we can come up with the conditional type {} extends Pick<T, K> ? T[K] : never:

Adding mapped type and conditional type
1type OptionalKeys<T> = keyof {
2 [K in keyof T]: {} extends Pick<T, K> ? T[K] : never;
3};
4
5type GetOptional<T> = Pick<T, OptionalKeys<T>>;

Let's check the solution in Playground – https://tsplay.dev/wenOaN

If we check it on any object type, we will see that it's not working correct

Checking current solution
1// "a" | "b"
2type Test1 = OptionalKeys<{ a?: 1; b: 2 }>;

But this is because we use keyof { [K in keyof T]: ... } which literally means keyof T. The conditional type is right but we need to return only optional keys. Let's change the solution slightly to make it work.

Return only optional keys

Let's change the mapped type a bit:

Return only optional keys
1type Values<T> = T[keyof T];
2
3type OptionalKeys<T> = Values<{
4 [K in keyof T]: {} extends Pick<T, K> ? K : never;
5}>;
6
7type GetOptional<T> = Pick<T, OptionalKeys<T>>;

What we did here:

  1. We added Values which returns not keys but values of the object type
  2. In OptionalKeys we now return key K instead of value T[K]. In combination with Values we can get the optional keys of T

The solution now works as expected https://tsplay.dev/WzL1rN

Is that it? Not really, let's look for the shorter solution 👀

Shorter solution

Currently we know that the main conditional type is {} extends Pick<T, K> ? K : never

Also we know there is Key Remapping via as in TypeScript 4.1 💡

Example of key remapping
1type MappedTypeWithNewKeys<T> = {
2 [K in keyof T as NewKeyType]: T[K];
3 // ^^^^^^^^^^^^^
4 // This is the new syntax!
5};

We can try it in our shorter solution, it will look like that:

Short solution
1type GetOptional<T> = {
2 [K in keyof T as {} extends Pick<T, K> ? K : never]: T[K];
3};

Let's sum up it again:

  1. We figured out how to identify optional keys and came up with the conditional type – {} extends Pick<T, K> ? K : never
  2. We added Values first and used it to infer optional keys
  3. Then we found the way to do it with key remapping via operator as

To be able to see the final solution with all the test cases, please have a look at the Playground – https://tsplay.dev/m05JGW

Thank you for your time! 🕛

Have a wonderful evening 🌆 and see you soon! 👋

typescript

Comments

Alexey Berezin profile image

Written by Alexey Berezin who loves London 🏴󠁧󠁢󠁥󠁮󠁧󠁿, players ⏯ and TypeScript 🦺 Follow me on Twitter