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.
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.