Search code examples
typescriptgenericsnullmapped-types

Apply a complex generic type recursively


Thanks to an answer from Nit, I have a generic type NullValuesToOptional that generates types where each nullable value becomes optional:

type NullValuesToOptional<T> = Omit<T, NullableKeys<T>> & Partial<Pick<T, NullableKeys<T>>>;

type NullableKeys<T> = NonNullable<({
  [K in keyof T]: T[K] extends NonNull<T[K]> ? never : K
})[keyof T]>;

type NonNull<T> = T extends null ? never : T;

It works:

interface A {
  a: string
  b: string | null
}
type B = NullValuesToOptional<A>; // { a: string, b?: string | null }

Now I would like to make NullValuesToOptional recursive:

interface C {
  c: string
  d: A | null
  e: A[]
}
type D = NullValuesToOptional<C>;
// { c: string, d?: NullValuesToOptional<A> | null, e: NullValuesToOptional<A>[] }

Is it possible?


Solution

  • Update: included TS 3.7 version + array types


    Do you mean something like this?

    TS 3.7+ (generic type arguments in arrays can now be circular):

    type RecNullValuesToOptional<T> = T extends Array<any>
      ? Array<RecNullValuesToOptional<T[number]>>
      : T extends object
      ? NullValuesToOptional<{ [K in keyof T]: RecNullValuesToOptional<T[K]> }>
      : T;
    

    Playground

    < TS 3.7 (a type resolution deferring interface is necessary):

    type RecNullValuesToOptional<T> = T extends Array<any>
      ? RecNullValuesToOptionalArray<T[number]>
      : T extends object
      ? NullValuesToOptional<{ [K in keyof T]: RecNullValuesToOptional<T[K]> }>
      : T;
    
    interface RecNullValuesToOptionalArray<T>
      extends Array<RecNullValuesToOptional<T>> {}
    

    Playground

    Test the type:

    interface A {
      a: string;
      b: string | null;
    }
    
    interface C {
      c: string;
      d: A | null;
      e: A[];
      f: number[] | null;
    }
    
    /*
    type EFormatted  = {
        c: string;
        e: {
            a: string;
            b?: string | null | undefined;
        }[];
        d?: {
            a: string;
            b?: string | null | undefined;
        } | null | undefined;
        f?: number[] | null | undefined;
    }
    
    => type EFormatted is the "flattened" version of 
    type E and used for illustration purposes here;
    both types E and EFormatted are equivalent, see also Playground
    */
    type E = RecNullValuesToOptional<C>
    

    Test with some data:

    const e: E = {
      c: "foo",
      d: { a: "bar", b: "baz" },
      e: [{ a: "bar", b: "qux" }, { a: "quux" }]
    };
    const e2: E = {
      c: "foo",
      d: { a: "bar", b: "baz" },
      e: [{ b: "qux" }, { a: "quux" }]
    }; // error, a missing (jep, that's OK)