I found this resource, which works great for types that don't have nested props. https://bobbyhadz.com/blog/typescript-remove-null-and-undefined-from-type
But in my case, I need to strip all props, even nested ones.
Is there any solution for doing that?
Note. My types are automatically generated in hundreds, so manually doing it is not an option.
Example type:
type BlogSlugQuery = {
__typename?: "Query" | undefined;
Blogs?: {
__typename?: "Blogs" | undefined;
docs?: ({
__typename?: "Blog" | undefined;
slug?: string | null | undefined;
} | null)[] | null | undefined;
} | null | undefined;
}
💡 I've come up with a way to do this that the resulting usage feels pretty readable to me. I extended another Stack Overflow answer by jcalz
const NotNullSymbol = Symbol("not null");
export type NotNull = typeof NotNullSymbol;
type RemoveNotNullTypes<T> = T extends NotNull
? unknown
: T extends object
? { [K in keyof T]: RemoveNotNullTypes<T[K]> }
: T;
type _Overwrite<T, U> = U extends NotNull
? Exclude<T, null>
: U extends object
? {
[K in keyof T]: K extends keyof U ? _Overwrite<T[K], U[K]> : T[K];
} & RemoveNotNullTypes<U>
: U;
type ExpandRecursively<T> = T extends Function
? T
: T extends object
? T extends infer O
? { [K in keyof O]: ExpandRecursively<O[K]> }
: never
: T;
export type Overwrite<T, U> = ExpandRecursively<_Overwrite<T, U>>;
type Person = {
name: string | null;
house: {
kitchen: {
stoveName: string | null;
stoveBrand: number | undefined;
otherThings: unknown;
};
};
};
type PersonWithNullsRemoved = Overwrite<
Person,
{
name: NotNull;
house: {
kitchen: {
stoveName: NotNull;
stoveBrand: string;
};
};
}
>;
function foo(person: PersonWithNullsRemoved) {
// no TS errors for the following lines
const name = person.name.toLowerCase();
const stoveName = person.house.kitchen.stoveName.toLowerCase();
const stoveBrand = person.house.kitchen.stoveBrand.toLowerCase();
}
function bar(person: Person) {
const name = person.name.toLowerCase(); // Error: Object is possibly 'null'
const stoveName = person.house.kitchen.stoveName.toLowerCase(); // Error: Object is possibly 'null'
const stoveBrand = person.house.kitchen.stoveBrand.toLowerCase(); // Error: Object is possibly 'undefined' and Error: Property 'toLowerCase' does not exist on 'number'.
}
I won't dive in to how Overwrite
works in general, since this was already done in the SO answer I was inspired by. I extended this with the NotNull
type in order to avoid having to override deeply nested properties like this: Exclude<Person['house']['kitchen']['stoveName'], null>
which can get pretty hectic when it's even more nested. Instead, simply NotNull
reads much better to me!
NotNull
is just the type of a specific unique symbol
. Alternatively a unique string const would probably suffice, but could lead to accidental matching.
When _Overwrite
evaluates the override map passed in, if the value is NotNull
then it will just take the value of the original type and exclude null
from it. Otherwise it follows the normal path if it's an object. However, when merging the object with U
we needed to make sure that the NotNull
type does not end up in the final type that is emitted. So we RemoveNotNullTypes
from U
and any of U
s nested properties.
This implementation is working well for us in a production environment where I override the type of an object returned by Prisma by removing null
s where the business logic does not allow for null
in the given situation. Sometimes you could get away with just adding !
to declare you don't expect it to be null
but in this situation we were trying to get the type emitted to match the ResponseBody
type from the generated Swagger Typescript API types.
Let me know if something still does not make sense and I'd be happy to try and explain further.
EDIT: I realized I did not answer the original question fully, since I was only removing null
(that's all I needed for my use-case).
const NotNullSymbol = Symbol("not null");
const NotNullIdentifier = { [NotNullSymbol]: true } as const;
export type NotNull = typeof NotNullIdentifier;
const NotUndefinedSymbol = Symbol("not undefined");
const NotUndefinedIdentifier = { [NotUndefinedSymbol]: true } as const;
export type NotUndefined = typeof NotUndefinedIdentifier;
type UtilityType = NotNull | NotUndefined;
type ExcludeSpecificUtilityType<T, U extends UtilityType> = U extends NotNull &
NotUndefined
? Exclude<T, null | undefined>
: U extends NotNull
? Exclude<T, null>
: U extends NotUndefined
? Exclude<T, undefined>
: never;
type RemoveUtilityTypes<T> = T extends UtilityType
? unknown
: T extends object
? { [K in keyof T]: RemoveUtilityTypes<T[K]> }
: T;
type _Overwrite<T, U> = U extends UtilityType
? ExcludeSpecificUtilityType<T, U>
: U extends object
? {
[K in keyof T]: K extends keyof U ? _Overwrite<T[K], U[K]> : T[K];
} & RemoveUtilityTypes<U>
: U;
type ReplaceOptionalWithUndefined<T> = T extends object
? {
[K in keyof Required<T>]: ReplaceOptionalWithUndefined<T[K]>;
}
: T;
type ExpandRecursively<T> = T extends Function
? T
: T extends object
? T extends infer O
? { [K in keyof O]: ExpandRecursively<O[K]> }
: never
: T;
export type Overwrite<T, U> = ExpandRecursively<
_Overwrite<ReplaceOptionalWithUndefined<T>, U>
>;
type PersonWithNullsRemoved = Overwrite<
Person,
{
name: NotNull;
house: {
kitchen: {
stoveName: NotUndefined & NotNull;
stoveBrand: string;
};
};
}
>;
Replacing undefined
is a little harder, since sometimes it can be represented as an optional not a specific type. I had help from this SO Answer to remove optionals and replace them with undefined.
Changed from just a symbol to an object with the symbol as a key. This was important to allow being able to create an intersection that represents both: NotNull & NotUndefined
.