TypeScript: remove key from type/subtraction type

68.8k views Asked by At

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

There are 7 answers

4
Adrian Leonhard On BEST ANSWER

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

1
JKillian On

Update: See Adrian's answer above for a solution to this question. I've left my answer here though since it still contains some useful links.


There are various old requests for this feature ("outersection" types, subtraction types), but none have really progressed.

Recently, with the addition of mapped types I asked about this again, and Anders said that while there's no plans to make a general subtraction type operator, a more limited version might be implemented, presumably looking something like this proposal.

I've personally run into quite similar situations to you when working with React, and unfortunately haven't been able to find any good solution. In a simple case, you can get away with something like:

interface BaseProps {
    foo: number;
    bar: number;
}

interface Inner extends BaseProps {
    cart: Cart;
}

interface Wrapper extends BaseProps {
    cartClient: Client;
}

but I almost find this to be a semantic abuse of the extends keyword. And of course, if you don't control the typings of Inner or BaseProps, then this won't work out.

2
Mathias On

While this has been correctly answered, I wanted to point out that TypeScript 3.5 did add an Omit<T, E> type.

type NoCart = Omit<{foo: string, bar: string, cart: number}, "cart">;

This results in the {foo: string, bar: string} type.

3
alter_igel On

Update for TypeScript 3.5: The Omit<Type, Keys> utility type is now available. Please see Mathias' answer for an example usage.


Old Answer: Since TypeScript 2.8 and the introduction of Exclude, It's now possible to write this as follows:

type Without<T, K> = {
    [L in Exclude<keyof T, K>]: T[L]
};

Or alternatively, and more concisely, as:

type Without<T, K> = Pick<T, Exclude<keyof T, K>>;

For your usage, you could now write the following:

type ExcludeCart<T> = Without<T, "cart">;
0
Milad Afkhami On

Probably all the answers are correct, but I built a shorter Subtract generic which does what you need:

type Subtract<T extends K, K> = Omit<T, keyof K>;
3
James McMahon On

So, for instance, ExcludeCart<{foo: number, bar: string, cart: number}> would be {foo: number, bar: string}

You can use the Exclude syntax to do this directly:

Exclude<{foo: number, bar: string, cart: number}, { cart: number}>
0
Félix Brunet On

there is another very simple way to have this result

When combining type in typescript, the type "never" have higher priority to everything.

You can simply create a type:

type noCart<T> = T & {cart : never}

Or, without creating type

function removeCart<T>(obj : T) : T & {cart : never} {
    if("cart" in obj) {
        delete (obj as T & {cart : any}).cart;
    }
    return <T & {cart : never}> obj;
}

This is less generic than the solution of Adrian, but a bit simpler when we don't need the complexity.