Home > Back-end >  Typescript optional property type not working on nested prop when it's an array of another type
Typescript optional property type not working on nested prop when it's an array of another type

Time:06-06

I made a type that makes all nullable properties optional, even nested ones but for some reason it doesn't work when given an array of another type.

Which is weird because it works when the array is "unwrapped" in the parent type. This are my types :

// returns null if T is nullable
type ExtractNull<T> = Extract<T, null> extends null
    ? Extract<T, null> extends never
        ? T
        : null
    : T
// returns an union type of all nullable keys in T
type NullKeys<T extends Record<PropertyKey, any>> = {
    [K in keyof T]: ExtractNull<T[K]> extends null
        ? K
        : T[K] extends Record<PropertyKey, any>
        ? NullKeys<T[K]>
        : never
}[keyof T]
// makes all nullable properties optional
export type OptionalNulls<T extends Record<PropertyKey, any>> = {
    [K in keyof T as Exclude<K, NullKeys<T>>]: T[K] extends Record<
        PropertyKey,
        any
    >
        ? OptionalNulls<T[K]>
        : T[K]
} & Partial<{
    [K in keyof T as Extract<K, NullKeys<T>>]: T[K]
}>

This is my test setup ("not optional" strings should be optional) :

type TestRoot = {
    prop0: null | string
    testObj: TestObj
    testObjArray: TestObj[]
    testObjArrayUnwrapped: [
        {
            prop1: null | string
            arr: [
                {
                    prop2: null | string
                }
            ]
        }
    ]
}
type TestObj = {
    prop1: null | string
    arr: [
        {
            prop2: null | string
        }
    ]
}
const test: OptionalNulls<TestRoot> = {
    prop0: "optional",
    testObj: {
        prop1: "optional",
        arr: [{ prop2: "optional" }],
    },
    testObjArray: [{ prop1: "not optional", arr: [{ prop2: "not optional" }] }],
    testObjArrayUnwrapped: [
        {
            prop1: "optional",
            arr: [
                {
                    prop2: "optional",
                },
            ],
        },
    ],
}

CodePudding user response:

I got it to work with two small modifications:

The NullKeys type you defined kind of breaks with tuples. To make it work properly I added one small change.

type NullKeys<T extends Record<PropertyKey, any>> = {
    [K in keyof T]: ExtractNull<T[K]> extends null
        ? K
        : T[K] extends Record<PropertyKey, any>
        ? NullKeys<T[K]>
        : never
}[keyof T & (T extends any[] ? `${bigint}` : string)] // <-- filter unwanted props

This will stop TypeScript from using all array properties like length and iterators from indexing this type.

type T0 = NullKeys<TestRoot>

// before:

// type T0 = number | "prop0" | "prop1" | "prop2" | (() => IterableIterator<"prop2">) | // (() => {
//     copyWithin: boolean;
//     entries: boolean;
//     fill: boolean;
//     find: boolean;
//     findIndex: boolean;
//     keys: boolean;
//     values: boolean;
// }) | ... 90 more ... |

// after: 

// type T0 = "prop0" | "prop1" | "prop2"

TypeScript will now complain about: Type instantiation is excessively deep and possibly infinite. But we can easily silence the compiler with this easy trick:

export type OptionalNulls<T extends Record<PropertyKey, any>> = {
    [K in keyof T as Exclude<K, NullKeys<T>>]: T[K] extends Record<
        PropertyKey,
        any
    >
        ? OptionalNulls<T[K]>
        : T[K]
} & Partial<{
    [K in keyof T as NullKeys<T> extends infer O ? Extract<K, O> : never]: T[K] // <-- silence compiler warning
}>

Playground


Yet I think you are overcomplicating the problem a bit. Here is a simpler solution:

type OptionalNulls<T> = {
    [K in keyof T as null extends T[K] ? K : never]?: T[K] extends object 
      ? OptionalNulls<T[K]> 
      : T[K]
} & {
    [K in keyof T as null extends T[K] ? never : K]: T[K] extends object 
      ? OptionalNulls<T[K]> 
      : T[K]
}

Playground

CodePudding user response:

I found this solution but @Tobias S. one is better

// returns null if T is nullable
type ExtractNull<T> = Extract<T, null> extends null
    ? Extract<T, null> extends never
        ? T
        : null
    : T
// returns an union type of all nullable keys in T
type NullableKeys<T extends Record<PropertyKey, any>> = {
    [K in keyof T]: ExtractNull<T[K]> extends null
        ? K
        : T[K] extends Record<PropertyKey, any>
        ? NullableKeys<T[K]>
        : never
}[keyof T]
// makes all nullable properties optional
export type OptionalNullable<T> = T extends Array<infer I>
    ? I extends ObjectRecord
        ? ({
                [K in keyof I as Exclude<
                    K,
                    NullableKeys<I>
                >]: I[K] extends Array<any> ? OptionalNullable<I[K]> : I[K]
          } & Partial<{
                [K in keyof I as Extract<K, NullableKeys<I>>]: I[K]
          }>)[]
        : I[]
    : T extends ObjectRecord
    ? {
            [K in keyof T as Exclude<
                K,
                NullableKeys<T>
            >]: T[K] extends Array<any> ? OptionalNullable<T[K]> : T[K]
      } & Partial<{
            [K in keyof T as Extract<K, NullableKeys<T>>]: T[K]
      }>
    : never
  • Related