reactjsgenericstypescriptdecoratorhigher-order-components

TypeScript: remove key from type/subtraction type


I want to define a generic type ExcludeCart<T> that is essentially T but with a given key (in my case, cart) removed. So, for instance, ExcludeCart<{foo: number, bar: string, cart: number}> would be {foo: number, bar: string}. Is there a way to do this in TypeScript?

Here's why I want to do this, in case I'm barking up the wrong tree: I'm converting an existing JavaScript codebase to TypeScript, which contains a decorator function called cartify that takes a React component class Inner and returns another component class Wrapper.

Inner should take a cart prop, and zero or more other props. Wrapper accepts a cartClient prop (which is used to generate the cart prop to pass to Inner), and any prop that Inner accepts, except cart.

In other words, once I can figure out how to define ExcludeCart, I want to do this with it:

function cartify<P extends {cart: any}>(Inner: ComponentClass<P>) : ComponentClass<ExcludeCart<P> & {cartClient: any}>

Solution

  • While there isn't a built-in subtraction type, you can currently hack it in:

    type Sub0<
        O extends string,
        D extends string,
    > = {[K in O]: (Record<D, never> & Record<string, K>)[K]}
    
    type Sub<
        O extends string,
        D extends string,
        // issue 16018
        Foo extends Sub0<O, D> = Sub0<O, D>
    > = Foo[O]
    
    type Omit<
        O,
        D extends string,
        // issue 16018
        Foo extends Sub0<keyof O, D> = Sub0<keyof O, D>
    > = Pick<O, Foo[keyof O]>
    

    In the question's case, you would do:

    type ExcludeCart<T> = Omit<T, 'cart'>
    

    With TypeScript >= 2.6, you can simplify it to:

    /**
     * for literal unions
     * @example Sub<'Y' | 'X', 'X'> // === 'Y'
     */
    export type Sub<
        O extends string,
        D extends string
        > = {[K in O]: (Record<D, never> & Record<string, K>)[K]}[O]
    
    /**
     * Remove the keys represented by the string union type D from the object type O.
     *
     * @example Omit<{a: number, b: string}, 'a'> // === {b: string}
     * @example Omit<{a: number, b: string}, keyof {a: number}> // === {b: string}
     */
    export type Omit<O, D extends string> = Pick<O, Sub<keyof O, D>>
    

    test it on the playground