Search code examples
typescript

The Omit type tool handles the issue of optional attributes


type User = {
  name?: string;
  age?: number;
  sex?: string;
};
type MyOmit<T, K extends PropertyKey> = { [P in Exclude<keyof T, K>]: T[P] };

type A = MyOmit<User, "name" | "age">;
type B = Omit<User, "name" | "age">;

Why type A = {sex:string|undefined} and type B = {sex?:string|undefined}? The underlying processing method of Omit type tools is MyOmit. Why do the results differ?


Solution

  • You encountered a TypeScript behavior known as (non-)homomorphic type mapping. In short, homomorphic mapping means that property modifiers such as readonly and optional (?) are preserved on the result of the mapped type. Check out this famous question for more information: What does "homomorphic mapped type" mean?

    A homomorphic mapped type is created when the compiler recognizes that the type to be mapped is an existing object type (=User in your example). That commonly happens when you e. g. use the in keyof syntax or in K where K extends keyof T and T is the object type to be mapped.


    On the other hand non-homomorphic mapping is applied when the compiler does NOT recognize your existing object type. This is the case when introducing other conditional types like Exclude or Extract. Therefore, TypeScript can't see K is actually related to User.

    As noted in microsoft/TypeScript/pull/12563 you can wrap MyOmit in a Pick for a workaround solution:

    type User = {
      name?: string;
      age?: number;
      sex?: string;
    };
    
    type MyOmit<T, K extends PropertyKey> = Pick<
      T,
      keyof { [P in Exclude<keyof T, K>]: T[P] }
    >;
    type A = MyOmit<User, "name" | "age">;
    //   ^? type A = { sex?: string | undefined; }
    

    TypeScript Playground