typescriptgraphql-codegen

Remove null and undefined from type (including nested props)


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;
}

Solution

  • 💡 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

    The Types

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

    Example Usage

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

    Explanation

    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 Us 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 nulls 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).

    Removing Null & Undefined

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

    Updated Example

    type PersonWithNullsRemoved = Overwrite<
      Person,
      {
        name: NotNull;
        house: {
          kitchen: {
            stoveName: NotUndefined & NotNull;
            stoveBrand: string;
          };
        };
      }
    >;
    

    More Explanation

    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.