Search code examples
arraystypescripttypesnull

Create a type helper to remove null from nested arrays


I have an interface like this:

interface SubBarText {
    text: string;
}
interface BarText {
    text: string;
    subBar?: SubBarText[] | null;
}

export interface Foo {
    bar: BarText[] | null;
    baz: {
        subBaz: string | null
    };
    boof: string;
}

I would like to use this to calculate a new type, removing null from all properties. From this answer I found this great one-liner to recursively remove null from properties and nested objects:

export type RemoveNull<Ob> = { [K in keyof Ob]: Ob[K] extends object ? RemoveNull<Ob[K]> : NonNullable<Ob[K]> };

but this is not able to remove null from objects in nested arrays (bar).

It works correctly if bar itself is null (all three properties throw type errors):

const CorrectlyFails: RemoveNull<Foo> = {
    bar: null,
    baz: { subBaz: null },
    boof: null
}

But it is not able to detect the null in subBar:

const shouldFailButDoesNot: RemoveNull<Foo> = {
    bar: [
        {
            text: 'asdf',
            subBar: null   // <-- this passes validation but should not
        }
    ],
    baz: { subBaz: null }, // <-- this correctly fails validation
    boof: null             // <-- this correctly fails validation
}

I'm able to work around it by manually constructing the type:

const CorrectlyFails2: Omit<RemoveNull<Foo>, "bar"> & { bar: RemoveNull<BarText[]> } = {
    bar: [
        {
            text: 'asdf',
            subBar: null
        }
    ],
    baz: { subBaz: null },
    boof: null
}

But I'm looking for a way to extend RemoveNull to deal with types like bar automatically.


Solution

  • The main problem with your RemoveNull<T> is that T[K] extends object ? RemoveNull<T[K]> : NonNullable<T[K]> doesn't do what you want when T[K] is a union of object and non-object types, like SomeObjType | null. That type does not extend object, so it resolves to the false branch, which would be NonNullable<SomeObjtype | null> or just SomeObjType, without recursing down into SomeObjType to remove nulls in there. Ideally you want your type to distribute over unions, so that RemoveNull<A | B | C> is evaluated as RemoveNull<A> | RemoveNull<B> | RemoveNull<C>. Then if you ever get SomeObjType | null, it will be processed as RemoveNull<SomeObjType> | RemoveNull<null>, the latter of which should just be never.

    So we can rewrite your type as a distributive conditional type:

    type RemoveNull<T> =
        T extends null ? never : { [K in keyof T]: RemoveNull<T[K]> };
    

    Now that this is distributive, T is broken into all its union members. Any of them that is null is dropped. (So we don't even need NonNullable in there, since null will be excluded by this check). The rest are fed to the mapped type. Note that it's a homomorphic mapped type (What does "homomorphic mapped type" mean?), so when it applies to primitives like string or number, it just returns its input. So it automatically does the right thing for primitives. If T is an array type then the homomorphic mapped type maps it to another array type. Finally, if T is a non-array object type, each property gets NonNull applied to it.

    Let's test it out:

    type X = RemoveNull<Foo>;
    /* type X = {
        bar: {
            text: string;
            subBar?: {
                text: string;
            }[] | undefined;
        }[];
        baz: {
            subBaz: string;
        };
        boof: string;
    } */
    

    Looks good. Everywhere that included null has had the null removed.

    Playground link to code