4

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?

Paleo
  • 21,831
  • 4
  • 65
  • 76
  • Wow, I [*just mentioned* recursive type definitions](https://stackoverflow.com/questions/58411739/typescript-no-repeat-value#comment103166962_58411862) (albeit in a different context) which actually got me wondering if it's even possible to have any kind of recursive type. And then this question pops up. For what it's worth, it seems like it should be possible in theory, at least. Not sure if it's implemented. – VLAZ Oct 16 '19 at 11:18

1 Answers1

2

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)
ford04
  • 66,267
  • 20
  • 199
  • 171