Search code examples
typescripttypescript-genericsmapped-typesconditional-types

Making properties of nested type optional


I am working with auto generated types from a library. The types contain all values as required whereas I would like to mark some of them as optional. I know this is possible using generics in typescript but not sure how to do it. Here is an example what I would like to happen:

So this is an example of type with nested types:

interface Person {
    name: string;
    hometown: string;
    nickname: string;
    data:{
        address:string,
        phone:number
    }
}

I am looking to invoke something like following to make the name property in root type and address property in nested type as optional:

type TransformedPerson = MakeOptional<Person,{name:string, data:{address:string}}>

or:

type TransformedPerson = MakeOptional<Person,"name"|"data.address">

Expecting to see the following type created:

/*
type TransformedPerson = {
    name?: string;
    hometown: string;
    nickname: string;
    data:{
        address?:string,
        phone:number
    }
}
*/

I've seen an example to use Partial in nested property with typescript for making properties optional in the root object but it does not work for nested type:

type RecursivePartial<T> = {
    [P in keyof T]?: RecursivePartial<T[P]>;
};

type PartialExcept<T, K extends keyof T> = RecursivePartial<T> & Pick<T, K>;

type TransformedType = PartialExcept<Person, "name"> /// This works only for root type

type TransformedType = PartialExcept<Person, "name"|"data.address"> /// throws error

Solution

  • Here's an approach based on a tuple that defines the optional keys:

    interface Person {
        name: string;
        hometown: string;
        nickname: string;
        data: {
            address: string;
            phone: number;
        }
    }
    
    type NestedKeys<T extends string, U extends string[]> = {
      [K in keyof U]: U[K] extends `${T}.${infer V}` ? V : never;
    }
    
    type PartialExcept<T, U extends string[]> = {
      [K in keyof T as K extends U[number] ? K : never]?: T[K]
    } & {
      [K in keyof T as K extends U[number] ? never : K]: K extends string
        ? PartialExcept<T[K], NestedKeys<K, U>>
        : T[K]
    }
    
    type TransformedPerson = PartialExcept<Person, ['name', 'data.address']>
    
    const p: TransformedPerson = {
      hometown: 'Sometown',
      nickname: 'Somename',
      data: {
        phone: 132456789
      }
    };
    

    The PartialExcept type defines a new type based on the intersection of two mapped types: one containing the optional properties of which the keys are present in the string tuple, and one containing the remaining required properties.

    The PartialExcept type uses itself recursively. For nested objects, the NestedKeys type is used to create a mapped tuple type that only contains the descendant keys of a given property.


    Playground link