Typescript: Add support for literal type subtraction

Created on 14 Nov 2016  ·  87Comments  ·  Source: microsoft/TypeScript

Now we have Mapped Types and keyof, we could add a subtraction operator that only works on literal types:

type Omit<T, K extends keyof T> = {
    [P in keyof T - K]: T[P];
};

type Overwrite<T, K> = K & {
    [P in keyof T - keyof K]: T[P];
};

type Item1 = { a: string, b: number, c: boolean };
type Item2 = { a: number };

type T1 = Omit<Item1, "a"> // { b: number, c: boolean };
type T2 = Overwrite<Item1, Item2> // { a: number, b: number, c: boolean };
Fixed Suggestion

Most helpful comment

Some fun stuff here. I just merged #16446 which allows an even simpler solution:

type Diff<T extends string, U extends string> = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T];
type Omit<T, K extends keyof T> = { [P in Diff<keyof T, K>]: T[P] };
type Overwrite<T, U> = { [P in Diff<keyof T, keyof U>]: T[P] } & U;

type T1 = Diff<"a" | "b" | "c", "c" | "d">;  // "a" | "b"

type Item1 = { a: string, b: number, c: boolean };
type Item2 = { a: number };

type T2 = Omit<Item1, "a"> // { b: number, c: boolean };
type T3 = Overwrite<Item1, Item2> // { a: number, b: number, c: boolean };

The Diff type is effectively a subtraction operator for string literal types. It relies on the following type equivalences:

  • T | never = T
  • T & never = never (which #16446 provides)

Furthermore, it relies on the fact that an object with a string index signature can be indexed with any string.

A native literal subtraction operator should include certain higher order type relationships. For example, given type parameters T and U, T - U should be considered a subtype of T, and { [P in keyof T - U]: T[P] } should be considered a supertype of T. The Diff type doesn't have those, so type assertions may in some cases be necessary.

All 87 comments

Duplicate of #4183?

I'm going to relabel this a suggestion as #4183 doesn't actually have a proposal.

@ahejlsberg checkout my proposal at #4183

Hope to see this soon, will be a huge improvement too!

Note that keyof T may be string (depending on available indexers), and therefore the behavior of string - 'foo', etc. will need to be defined.

Edit: corrected that keyof T is always string, however the point of string - 'foo' remains.

Note that keyof T may be string or number or string | number (depending on available indexers), and therefore the behavior of string - 'foo', and number - '1', etc. will need to be defined.

this is not accurate. keyof is always a string.

If there are any questions about use cases, this would be incredibly helpful for typing Redux, Recompose, and other higher order component libraries for React. For instance, wrapping an uncontrolled dropdown in a withState HOC removes the need for isOpen or toggle props, without the need to manually specify a type.

Redux's connect() similarly wraps and supplies some/all props to a component, leaving a subset of the original interface.

Poor man's Omit:

type Omit<A, B extends keyof A> = A & {
  [P in keyof A & B]: void
}

The "omitted" key is still there, however mostly unusable, since it's type is T & void, and nothing sane can satisfy this constraint. It won't prevent you from accessing T & void, but it will prevent you from assigning it or using it as a parameter in a function.

Once https://github.com/Microsoft/TypeScript/pull/13470 lands, we can do even better.

@niieani That solution won't work for react HoC that inject props.

If you use this pattern to "remove" props, the typechecker will still complain if you omit them when using a component:

type Omit<A, B extends keyof A> = A & {
  [P in keyof A & B]: void
}

class Foo extends React.Component<{a:string, b:string}, void> {
}

type InjectedPops = {a: any}
function injectA<Props extends InjectedPops>(x:React.ComponentClass<Props>):React.ComponentClass<Omit<Props, 'a'>>{
  return x as any
}

const Bar = injectA(Foo)

var x = <Bar b=''/>
// Property 'a' is missing in type 'IntrinsicAttributes & IntrinsicClassAttributes<Component<Omit<{ a: string; b: string; }, "a">, Co...'.

I think this one would be easier to realise in the following form:

[P in keyof A - B] should work only if A extends B, and return all keys that are in A and are not in B.

type Omit<A extends B, B> = { 
  [P in keyof A - B]: P[A] 
}

type Impossible<A, B> = {
 [P in keyof A - B]: string
} /* ERROR: A doesn't extend B */

interface IFoo {
   foo: string
}

interface IFooBar extends IFoo {
  bar: string
}

type IBar = Omit<IFooBar, IFoo>; // { bar: string }

Since we cannot change types of fields when extending, all inconsistencies (a.k.a) string - 'world' would go away.

Overwrite seems covered by #10727. This proposal seems an alternate syntax for #13470?

13470 is not the same, since it does not allow you to dynamically create difference types.

Overwrite using today's syntax:

type Item1 = { a: string, b: number, c: boolean };
type Item2 = { a: number };

type ObjHas<Obj extends {}, K extends string> = ({[K in keyof Obj]: '1' } & { [k: string]: '0' })[K];
type Overwrite<K, T> = {[P in keyof T | keyof K]: { 1: T[P], 0: K[P] }[ObjHas<T, P>]};
type T2 = Overwrite<Item1, Item2> // { a: number, b: number, c: boolean };

Edit: fixed an indexing issue.

I also tried my hand at Omit, if with mixed success:

// helpers...
type Obj<T> = { [k: string]: T };
type SafeObj<O extends { [k: string]: any }, Name extends string, Param extends string> = O & Obj<{[K in Name]: Param }>;
type SwitchObj<Param extends string, Name extends string, O extends Obj<any>> = SafeObj<O, Name, Param>[Param];
type Not<T extends string> = SwitchObj<T, 'InvalidNotParam', {
  '1': '0';
  '0': '1';
}>;
type UnionHas<Union extends string, K extends string> = ({[S in Union]: '1' } & { [k: string]: '0' })[K];
type Obj2Keys<T> = {[K in keyof T]: K } & { [k: string]: never };

// data...
type Item1 = { a: string, b: number, c: boolean };

// okay, Omit, let's go.
type Omit_<T extends { [s: string]: any }, K extends keyof T> =
  {[P2 in keyof T]: { 1: Obj2Keys<T>[P2], 0: never }[Not<UnionHas<K, P2>>]}
type T1 = Omit_<Item1, "a">;
// intermediary result: { a: never; b: "b"; c: "c"; }
type T2 = {[P1 in T1[keyof Item1] ]: Item1[P1]}; // { b: number, c: boolean };
// ^ great, the result we want!

Wonderful, yet another problem solved!
...

// now let's combine the steps?!
type Omit<T extends { [s: string]: any }, K extends keyof T> =
  {[P1 in {[P2 in keyof T]: { 1: Obj2Keys<T>[P2], 0: never }[Not<UnionHas<K, P2>>]}[keyof T] ]: T[P1]};
type T3 = Omit<Item1, "a">;
// ^ yields { a: string, b: number, c: boolean }, not { b: number, c: boolean }
// uh, could we instead do the next step in a separate wrapper type?:
type Omit2<T extends { [s: string]: any }, K extends keyof T> = Omit_<T, K>[keyof T];
// ^ not complete yet, but this is the minimum repro of the part that's going wrong
type T4 = Omit2<Item1, "a">;
// ^ nope, foiled again! 'a'|'b'|'c' instead of 'b'|'c'... wth? 
// fails unless this access step is done after the result is calculated, dunno why

Note that my attempt to calculate union differences in #13470 suffered from the same problem... any TypeScript experts in here? 😄

@tycho01 what you're doing here is amazing. I've played around with it a bit and the 1-step behavior does seem like a bug in TypeScript. I think if you file a separate bug report about it, we could get it solved and have a wonderful one-step Omit and Overwrite! :)

@niieani: yeah, issue filed at #16244 now.

I'd scribbled a bit on current my understanding/progress on TS type operations; just put a pic here. Code here.
It's like, we can do boolean-like operations with strings, and for unions/object operations the current frontier is overcoming this difference glitch.

Operations on actual primitives currently seem off the table though, while for tuple types things are still looking tough as well -- we can get union/object representations using keyof / Partial, and hopefully difference will work that way too. I say 'hopefully' because some operations like keyof seem not to like numerical indices...

Things like converting those back to tuple types, or just straight type-level ... destructuring, aren't possible yet though.
When we do, we may get to type-level iteration for reduce-like operations, where things get a lot more interesting imo.

Edit: strictNullChecks issue solved.

@tycho01 ,
Amazing thing indeed!!

I played with it a bit, what do you think about this solution?

type Obj<T> = { [k: string]: T };
type SafeObj<O extends { [k: string]: any }, Name extends string, Param extends string> = O & Obj<{[K in Name]: Param }>;
type SwitchObj<Param extends string, Name extends string, O extends Obj<any>> = SafeObj<O, Name, Param>[Param];
type Not<T extends string> = SwitchObj<T, 'InvalidNotParam', {
  '1': '0';
  '0': '1';
}>;
type UnionHas<Union extends string, K extends string> = ({[S in Union]: '1' } & { [k: string]: '0' })[K];
type Obj2Keys<T> = {[K in keyof T]: K } & { [k: string]: never };

type Omit_<T extends { [s: string]: any }, K extends keyof T> =
  {[P2 in keyof T]: { 1: Obj2Keys<T>[P2], 0: never }[Not<UnionHas<K, P2>>]}

export type Omit<T extends { [s: string]: any }
    , K extends keyof T
    , T1 extends Omit_<T, K>= Omit_<T, K>
    , T1K extends keyof Pick<T1, keyof T>=keyof Pick<T1, keyof T>> =
    {[P1 in T1[T1K]]: T[P1]}
  ;

type Item1 = { a: string, b: number, c: boolean };

const ommited: Omit<Item1, 'a'> = {
  b: 6,
  c: true,
}

@nirendy seems to be working for me! 👍
@tycho01 great job on solving the strictNullChecks issue.

@niieani: you can thank @jaen for that, see #13470. 😃

@nirendy: whoa! I hadn't realized we can use this defaults syntax like that. the repetition feels a bit awkward, but being able to save/reuse results and check them for debugging purposes is pretty awesome!

That said... separating the steps here was one thing, but also swapping out the [keyof T] for that keyof Pick<>? How did you ever figure any of this out?! 😆

that's amaze balls guys, how did you do it?

@tycho01
I feel like there is no way to avoid that repetition there (while using the current version tools) since it plays a different role each time (once it's there to make sure the generic actually extends the type and the other one is there in order to omit the need of providing all the generics params).

Any way, I think we managed to achieve an amazing thing!
It doesn't really matter to me that the code came out pretty awkward, I'll just place it in a declaration file and use it in a similar way of how I use the Pick, Partial, etc... types.

Amazing!

@nirendy: yeah, no complaints here! 😃

edit: @aleksey-bykov: as for my parts at least, step by step, from the ground up (see #16392).

Some fun stuff here. I just merged #16446 which allows an even simpler solution:

type Diff<T extends string, U extends string> = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T];
type Omit<T, K extends keyof T> = { [P in Diff<keyof T, K>]: T[P] };
type Overwrite<T, U> = { [P in Diff<keyof T, keyof U>]: T[P] } & U;

type T1 = Diff<"a" | "b" | "c", "c" | "d">;  // "a" | "b"

type Item1 = { a: string, b: number, c: boolean };
type Item2 = { a: number };

type T2 = Omit<Item1, "a"> // { b: number, c: boolean };
type T3 = Overwrite<Item1, Item2> // { a: number, b: number, c: boolean };

The Diff type is effectively a subtraction operator for string literal types. It relies on the following type equivalences:

  • T | never = T
  • T & never = never (which #16446 provides)

Furthermore, it relies on the fact that an object with a string index signature can be indexed with any string.

A native literal subtraction operator should include certain higher order type relationships. For example, given type parameters T and U, T - U should be considered a subtype of T, and { [P in keyof T - U]: T[P] } should be considered a supertype of T. The Diff type doesn't have those, so type assertions may in some cases be necessary.

I noticed a small technical difference in that the cleaner Overwrite definition yields an intersection type:

type Diff<T extends string, U extends string> = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T];
type Overwrite<T, U> = { [P in Diff<keyof T, keyof U>]: T[P] } & U;
type Item1 = { a: string, b: number, c: boolean };
type Item2 = { a: number };
type T3 = Overwrite<Item1, Item2> // { b: number, c: boolean } & { a: number }

The following alternative, adding an extra Pick step, appears to 'fix' that, for whatever it's worth.

type Overwrite<T, U, Int = { [P in Diff<keyof T, keyof U>]: T[P] } & U> = Pick<Int, keyof Int>;

... as far as I know that seems little more than an aesthetic difference. oh well!

Edit: I take that back; I no longer recommend using this Pick-based version. It actually appears to subtly break a few things like my attempts at re-typing Ramda's fromPairs / zipObject here.

I tried using this (the one from @tycho01 and/or @nirendy, not the new one from @ahejlsberg yet) to omit a property from a class that overrides toString(), and it didn't work. Here's a minimal example:

type Item1 = { a: string, b: number, c: boolean, toString(): string };
type OmittedItem1 = Omit<Item1, "a"> 
// OmittedItem1 should be { b: number, c: boolean, toString(): string }, but is {}!

I can kind of imagine why overriding toString() and company (uh... valueOf()? maybe all other prototype methods?) would wreak havoc on anything that enumerates keys, but it seems unfortunate.

Anyone want to speculate whether this is intended behavior of Omit, a bug in Omit, or a bug in Typescript that breaks Omit?

@jcalz:

Yeah, I've had considerable issues with toString (and other Object.prototype method toLocaleString).
I'm not getting {} though -- which TS version? Playground and newer seem better.

Edit: the problem appear to lie in Diff: it works by doing & { [P in U]: never } & { [x: string]: never }. In the event of toString though, these two object types are expected to have a toString method, which messes them up. I'm gonna omit toLocaleString for brevity.

For the latter, it seems possible to do say { toString: "toString"; [k: string]: never } to overwrite it. The tough thing is like, I don't think you can make it never here cuz that'd get overwritten by the prototype method...

For the former though, a syntax like { toString: "something"; [P in U]: never } appears not to work for me, yielding A computed property name must be of type 'string', 'number', 'symbol', or 'any', or having the latter half seemingly getting ignored.
A syntax { [P in keyof U]: never; [K in keyof PrototypeMethods]: "something"; } would be even more desirable here but again no dice. Then again, the question is still what we'd overwrite it with rather than "something", since you'd just wanted whatever was otherwise specified, but either way even when you wanna overwrite toString as never, you can't. Essentially just about all operations currently get screwed up with toString.

I suppose combining these object syntaxes has been not exactly common, and this case might not actually be supported.
... Any comment on that, @ahejlsberg?

Still kinda thinking about alternatives... it's definitely a tough one though.

Edit 2: one issue here is ambiguity in { [k: string]: 123 }['toString'] -- should toString be expected to be among k, resolving to 123, or should it just resolve to the prototype method?
Currently TS presumes the latter, which makes it hard to serve use-cases where you intend the former.
Essentially I'd wanna try to construct an alternative way to access object properties that is immune to this, instead checking only own keys rather than prototype methods as well.
I've made a bit of progress, but it's hard to make anything without using the original property access operator, so kinda need to go over things to prevent buggy behavior from sneaking in everywhere.

I'm not getting {} though -- which TS version? Playground and newer seem better.

Demonstration

This is amazing! It appears that @nirendy's version doesn't work with optionals:

type Item1 = { a: string, b?: number, c: boolean }
const omitted: Omit<Item1, 'a'> = { c: true } 
// Property 'b' is missing in type '{ c: true; }'.

@jcalz: late response but thanks for the repro. seems resolving it may not be trivial though since property access and most operations depending on it are affected; will try further at one point.

@dleavitt:
Thanks for the bug report. I tried a bit; looks like, as you noted, Omit (either version) doesn't currently reproduce the ?. With strictNullChecks it instead appends a | undefined to the value, which seems not good enough yet to make this type-check. I'm not yet sure of a way to address this that wouldn't require type-level type checks (potentially solved by e.g. #6606) to check whether undefined would satisfy the object properties...

@tycho01 could we solve the ? issue by typing it multi-pass?

i.e.:

  1. make the optional properties non-optional
  2. stash the list of optional properties only, in a generic, aside
  3. do the Omit on the non-optional type
  4. re-add the | undefined to the existent keys of the omitted type

@niieani:

  1. re-add the | undefined to the existent keys of the omitted type

This doesn't actually cut it; with strictNullChecks we already get back { b: number | undefined; c: boolean; }, so the ? appears to matter.

I'll need to admit I don't fully understand what's going on yet. If you use { [P in keyof Item1]: P } the ? appears preserved just fine, meaning the info appears available in keyof Item1, though if you check that itself you don't get to see any additional info. Looks like a magical invisible property, not having checked compiler internals.

2: stash the list of optional properties only, in a generic, aside

Checking whether a property is optional is currently a challenge. Doing a keyof seems not to give this info (in a visible way); number | undefined is clear to us, but the amount of type-level operators is severely limited -- if we know undefined satisfies that type we're good, but on the type level, as it stands, we don't have a way to do that.

// insert `Omit` typing, one or the other depending on whether you use Playground or 2.5

type Item1 = { a: string, b?: number, c: boolean }
type noA = Omit<Item1, 'a'>
// { b: number | undefined; c: boolean; }
// ^ `Omit` degenerates `?` to ` | undefined`
type keys = keyof Item1
// "a" | "b" | "c"
type almost1 = { [P in keyof Item1]: If<ObjectHasKey<noA, P>, (noA & Obj<never>)[P], never> }
// { a: never; b?: number | undefined; c: boolean; }
type almost2 = { [P in keyof Item1]: If<ObjectHasKey<noA, P>, P, never> }
// { a: never; b?: "b"; c: "c"; }

The thing that gets us 'stuck' at that point is that the only way to ditch a: never prop is by plugging almost2's keys into itself, though that would simultaneously make us lose out on b's ? status. In almost1 we're closer to where we wanna go, but no closer to ditching a in a way that'd leave b's ? intact.

While we cannot directly detect or operate on ? itself, operating on it by proxy through checks for | undefined seems a fair enough solution. But yeah, no type-level type checks. Short of upvoting that feature request (or alternatives, I recently saw a comment requesting type-level instanceof + boolean-based type-level conditionals), I dunno.

@niieani: alternatively, say | undefined should be considered equivalent to ? and file as bug. That could be controversial (I'm not sure if I'd consider it more elegant myself), but it'd be less involved for the TS side so for your purposes potentially better chances of a fix.

@tycho01 yeah, I see it. I think the TypeScript team mentioned somewhere that | undefined should be considered equivalent to ? -- i.e. it aligns with their goals. For most intents and purposes this is how TS currently behaves, although I see what you mean by compiler differentiating somehow.

For example Flow treats these (? and | void) as different things, i.e. you could technically have a property and expect its type to be undefined. However for (all?) practical reasons they are pretty much the same.

@niieani: Yeah. Seems appropriate to file a bug then.

excuse me for being off topic

@jcalz practically speaking toString can be erased from the declaration files, this way it will only appear on the classes/objects that explicitly implement it (provided the default implementation is useless anyway)

this is how it can be done:

  1. set noLib: true in tsconfig.json
  2. get a copy of declaration files you need from https://github.com/Microsoft/TypeScript/tree/master/src/lib and include them to your project
  3. find toString of the Object and comment it out
interface Object {
    /** The initial value of Object.prototype.constructor is the standard built-in Object constructor. */
    constructor: Function;

    /** Returns a string representation of an object. */
    // toString(): string; <--- HERE

@dleavitt @niieani filed it at #16762.

@tycho01 @dleavitt using Pick preserves optionals in both versions of Omit. I'm not sure if this breaks anything else though.

Here's a full example with the concise 2.4 definition.

type Diff<T extends string, U extends string> = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T];
type Omit<T, K extends keyof T> = Pick<T, Diff<keyof T, K>>;

interface A {
  a: string;
  b?: string;
  c: number;
}

type B = Omit<A, 'c'>;

const a: A = {
  a: '5',
  c: 5,
};

const b: B = {
  a: '5',
};

// Note: typeof B is (correctly) Pick<A, 'a' | 'b'>

Similarly, you can redefine Omit in the legacy version (rest of the definition is _omitted_).

type Omit<T extends { [s: string]: any }
    , K extends keyof T
    , T1 extends Omit_<T, K>= Omit_<T, K>
    , T1K extends keyof Pick<T1, keyof T> = keyof Pick<T1, keyof T>> = Pick<T, T1[T1K]>;

@Pinpickle: I tried for a bit with the other tests I had. I only see your Omit definition fixing issues, no cons found! 😃

@Pinpickle this is also working "well enough" with the toString() override, or at least workaroundably so. ("Workaroundably" is a word, right?)

type Item1 = { a: string, b: number, c: boolean, toString(): string }; 
type OmittedItem1 = Omit<Item1, "a">; // { b: number, c: boolean } almost?
type OmittedItem1Fixed = OmittedItem1 & { toString(): string }; // good enough

I was reminded by a comment in #12424, current Omit and Overwrite implementations still fail recreate any string index the source object types might have held. Checking for index presence could be done given #6606.

@ahejlsberg @Pinpickle and others - thanks with the help on this! Using the above technique, we were able to replicate flow's $Diff type. This allowed us to correctly handle defaultProps in React:

/**
 * From https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-307871458
 * The Diff type is a subtraction operator for string literal types. It relies on:
 *  - T | never = T
 *  - T & never = never
 *  - An object with a string index signature can be indexed with any string.
 */
export type StringDiff<T extends string, U extends string> = ({[K in T]: K} &
  {[K in U]: never} & {[K: string]: never})[T];

/**
 * From https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-311923766
 * Omits keys in K from object type T
 */
export type ObjectOmit<T extends object, K extends keyof T> = Pick<T, StringDiff<keyof T, K>>;

/**
 * Returns a version of type T where all properties which are also in U are optionalized.
 * Useful for makding props with defaults optional in React components.
 * Compare to flow's $Diff<> type: https://flow.org/en/docs/types/utilities/#toc-diff
 */
export type ObjectDiff<T extends object, U extends object> = ObjectOmit<T, keyof U & keyof T> &
  {[K in (keyof U & keyof T)]?: T[K]};

deleted question made before switching brain to javascript mode :-) (sorry for the spam)

@rob3c:

type OmitFromBoth<T, K extends keyof T> = Omit<T & typeof T, K | 'prototype'>;
type WorksForBoth = OmitFromBoth<Hmm, 'I1' | 'S1'>;
// "I1" | "S2"

Like that? Untested, but you just wanted your last example shortened right?

@tycho01 I tried that already as well as other variations, but none worked. There doesn't seem to be a way to reference T's prototype within the generic definition. That's probably due to javascript not allowing access to 'static' prototype properties from an instance (unlike, say, Java). That's why I deleted the question, but you answered too fast :-)

@rob3c: So no typeof on T huh? I see what you're getting at then; I hadn't really tried things like that so far. I'll admit I'm not aware of workarounds in that case. I'm slowly discovering debugging with the compiler is a bit less bad than I thought, but still not too great.

@robc there actually are ways to access prototype in a generic context (I'm now curious about what the question was 😁).

EDIT typo fixed

@aluanhaddad The question was essentially about writing a version of Omit above that returns filtered keyof properties for both instance and static members of a type while only needing to write Omit<MyClass, ...> and not Omit<MyClass & typeof MyClass, ...>. I was saying prototype above, but it should have been constructor, since that's where the 'static' members are stuck by typescript (I think I got distracted by trying to filter out the 'prototype' property in my code and the example). And for various reasons documented elsewhere, the constructor property is only typed as Function, so all variations I tried that pulled keys from the 'constructor' member only resulted in those on Function without the static props declared in the class.

Anyway, although interesting in terms of generic type wrangling, it's irrelevant in my particular case, since you can't directly access static members from an instance anyway in javascript lol (I blame it on momentary language blindness due to insufficient coffee.)

The original code was something like this:

type Diff<T extends string, U extends string> = (
    & { [P in T]: P }
    & { [P in U]: never }
    & { [x: string]: never }
)[T];

type Omit<T, K extends keyof T> = Pick<T, Diff<keyof T, K>>;

class Hmm {
    readonly I1 = 'I1';
    readonly I2 = 'I2';
    static readonly S1 = 'S1';
    static readonly S2 = 'S2';
};

// "I2"
type WorksForInstancePropsOnly = base.Omit<Hmm, 'I1'>;

/// "prototype" | "S1" | "S2"
type HasAnnoyingPrototypeKey = keyof typeof Hmm;

// "S2"
type WorksForStaticPropsOnly = base.Omit<typeof Hmm, 'S1' | 'prototype'>;

// "I1" | "S2"
type WorksForBoth = base.Omit<Hmm & typeof Hmm, 'I1' | 'S1' | 'prototype'>;

type OmitForBoth<T, ...> = ???

@rob3c

Anyway, although interesting in terms of generic type wrangling, it's irrelevant in my particular case, since you can't directly access static members from an instance anyway in javascript lol (I blame it on momentary language blindness due to insufficient coffee.)

that statement is inaccurate.

it is possible ofc

class Hmm {
  static s1 = 'static one'
  static s2 = 'static two'
  i1 = 'instance one'
  i2 = 'instance two'
}

const foo = new Hmm()

console.log(foo)

const {s1, s2} = foo.constructor

console.log({s1, s2})

@Hotell

that statement is inaccurate.

Actually, my statement IS accurate, but you may have misunderstood my terminology :-)

Unlike Java, for example, you can't directly access static members on an instance (e.g. myInstance.myStaticProp). Your example shows an attempt at indirect access via the instance's constructor property - which typescript will complain about btw with corresponding red squigglies in vscode, because that's only available at runtime. That's why I lamented constructor being only typed as Function above, instead of something more strongly-typed (see https://github.com/Microsoft/TypeScript/issues/3841 and related).

20170816 constructor prop screenshot for github thread small

Anyway, the question pertains to generic type definitions using keyof and such for the purposes of strongly-typed intellisense and overall type-checking while writing code, not what's really available at runtime. Since the intention behind my use of keyof was for strongly-typing the available declared directly-accessible properties, their availability on constructor at runtime is irrelevant (as stated above).

Even with respect to just the theoretical question on whether it's possible to combine both in a generic type definition using only T, the answer is no without explicitly typing constructor on the classes of interest, since it's only typed Function in general.

20170816 constructor prop screenshot 2 intellisense errors small

I'm trying to use Omit type as suggested by @aldendaniels in this comment:

export type RegionsTableProps = 
ObjectOmit<TableProps, 'headerHeight' | 'rowHeight' | 'rowCount'> & 
{
  regions: Region[];
  onRegionClick: (id: string) => void;
};

class RegionsTable extends React.PureComponent<RegionsTableProps> {}

where TableProps is from @types/react-virtualized (width?: number;)

However, when I try to use RegionsTable like this:

  table = ({ width, height }: Dimensions) => (
    <RegionsTable
      regions={this.props.regions.list}
      onRegionClick={this.onRegionClick}
      width={width}
      height={height}
    />
  );

I get error:

TS2339: Property 'width' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<RegionsTable> & Readonly<{ children?: ReactNode; }...'.

If I replace ObjectOmit<TableProps, 'headerHeight' | 'rowHeight' | 'rowCount'> with Partial<TableProps> everything compiles.

I don't quite understand what is going on and how to fix this. Anyone got any ideas?

@doomsower it's probably due to [key: string]: any in GridCoreProps

// A = never 
export type A = StringDiff<keyof TableProps, 'headerHeight' | 'rowHeight' | 'rowCount'>

(ts 2.4.2)

@ahejlsberg Diff, Omit and Overwrite are awesome, can we expect them to make it into the standard library at some point alongside Record, Partial, Readonly, Pick, etc.?

for Overwrite, which version would be considered preferable?

For those interested, I've added Diff, Omit and Overwrite to Type Zoo.

@goodmind the more the merrier

Agreed, while we've yet to even settle on all the right best practices we definitely need more competition. Type libs still face significant issues in testing with e.g. non-terminating types (usually bugged recursive types) -- rather than having a single unit test fail your whole compilation just fails to terminate. I'd love to see more people try their own take on things on whatever level.

I've noticed an issue with Omit and generic functions, or at least with one of the implementations. Not quite sure how to describe it, so I'll stick with a code sample:

type Diff<T extends string, U extends string> = ({ [P in T]: P } &
  { [P in U]: never } & { [x: string]: never })[T];
type Omit<T, K extends keyof T> = { [P in Diff<keyof T, K>]: T[P] };

interface BaseType {
  x: string;
}

const fnWithDefault = <T extends BaseType>(
  input: Omit<T, keyof BaseType>
): T => {
  // Error: Spread types may only be created from object types
  const attempt1 = { ...input, x: 'foo' };
  // Error: Type 'Omit<T, "x"> & { x: string; }' is not assignable to type 'T'.
  return Object.assign({}, input, { x: 'foo' });
};

fnWithDefault<{ x: string; y: string }>({ y: 'bar' });

fnWithDefault is a function that adds x to the result, returning T, and thus the input type should be T without x. TypeScript, however, doesn't seem to allow this with either spread or Object.assign.

Basically, I would expect Omit<T, 'x'> & { x: string } to be assignable to T. (assuming that T.x is a string). Not sure if this is a problem in TypeScript, or with the Omit implementation, or is even by design.

Getting back for a while to the Omit implementations. I've created a minimal example with various implementations of Omit. Then I test them on interface with optional and readonly fields. One of them, as someone mentioned earlier, doesn't respect optional fields. So, I tested it again, substituting parts of Omit with underlaying implementations. Look at the code below:

// built-in Pick definition for reference
// type Pick<T, K extends keyof T> = {[P in K]: T[P]; };

// well-known Diff implementation
type Diff<T extends string, U extends string> = ({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T];

// our example interface with some optional fields
interface Example {
    a?: number;
    readonly b: boolean;
    c?: string;
    readonly d: any;
}

// Omit implementation which doesn't respect optional fields and readonly
type Omit<T, K extends keyof T> = {[P in Diff<keyof T, K>]: T[P]};
type ExampleSubsetA = Omit<Example, 'a' | 'b'>;
// ExampleSubsetA = { c: string; d: any; }

// Omit implementation using Pick, which works good with optional fields and readonly
type OmitUsingPick<T, K extends keyof T> = Pick<T, Diff<keyof T, K>>;
type ExampleSubsetB = OmitUsingPick<Example, 'a' | 'b'>;
// ExampleSubsetB = { c?: string; readonly d: any; }

// ok, so we now substitute Diff with its body, resulting in
// Omit implementation with Pick but without Diff, that still works good with optional fields and readonly
type OmitUsingPickWithoutDiff<T, K extends keyof T> = Pick<T, ({[P in keyof T]: P } & {[P in K]: never } & { [x: string]: never })[keyof T]>;
type ExampleSubsetC = OmitUsingPickWithoutDiff<Example, 'a' | 'b'>;
// ExampleSubsetC = { c?: string; readonly d: any; }

// well, so now we substitute Pick with its body resulting in
// Omit implementation without any other interfaces, which suddenly stopped respecting optional fields and readonly
type OmitSelfsufficient<T, K extends keyof T> = {[P in ({[P in keyof T]: P } & {[P in K]: never } & { [x: string]: never })[keyof T]]: T[P]};
type ExampleSubsetD = OmitSelfsufficient<Example, 'a' | 'b'>;
// ExampleSubsetD = { c: string; d: any; }

(Code on TypeScript Playground)

My questions is, why getting rid of Pick results of optional and readonly modifiers being lost in process? For me it seems like some bug in TypeScript itself.

It looks like there was a recent change in TypeScript@next that breaks a popular implementation of Diff and Omit:

type Diff<T extends string, U extends string> = ({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T]
type Omit<T, K extends keyof T> = Pick<T, Diff<keyof T, K>>

It has been working for a while, but now errors (using dtslint which is pulling latest typescript@next

ERROR: 2:45  expect  TypeScript@next compile error:
Type '({ [P in T]: P; } & { [P in U]: never; } & { [x: string]: never; })[keyof T]' does not satisfy the constraint 'keyof T'.
  Type '({ [P in T]: P; } & { [P in U]: never; })[keyof T]' is not assignable to type 'keyof T'.

I tried various Omit implementations listed by @aczekajski and they get various errors:

type Omit<T, K extends keyof T> = {[P in Diff<keyof T, K>]: T[P]};
ERROR: 1:44  expect  TypeScript@next compile error:
Type '({ [P in T]: P; } & { [P in U]: never; } & { [x: string]: never; })[keyof T]' is not assignable to type 'string'.
  Type '({ [P in T]: P; } & { [P in U]: never; })[keyof T]' is not assignable to type 'string'.
ERROR: 1:63  expect  TypeScript@next compile error:
Type 'P' cannot be used to index type 'T'.



md5-b82dbd8360a59e843eda22c0d24ea876



ERROR: 1:54 expect TypeScript@next compile error:
Type '({ [P in T]: P; } & { [P in U]: never; } & { [x: string]: never; })[keyof T]' does not satisfy the constraint 'keyof T'.
Type '({ [P in T]: P; } & { [P in U]: never; })[keyof T]' is not assignable to type 'keyof T'.

```ts
type OmitUsingPickWithoutDiff<T, K extends keyof T> = Pick<T, ({[P in keyof T]: P } & {[P in K]: never } & { [x: string]: never })[keyof T]>
ERROR: 1:65  expect  TypeScript@next compile error:
Type '({ [P in keyof T]: P; } & { [P in K]: never; } & { [x: string]: never; })[keyof T]' does not satisfy the constraint 'keyof T'.
  Type '({ [P in keyof T]: P; } & { [P in K]: never; })[keyof T]' is not assignable to type 'keyof T'.



md5-220859cd669f13e25d02f34222d801f3



ERROR: 1:58 expect TypeScript@next compile error:
Type '({ [P in keyof T]: P; } & { [P in K]: never; } & { [x: string]: never; })[keyof T]' is not assignable to type 'string'.
Type '({ [P in keyof T]: P; } & { [P in K]: never; })[keyof T]' is not assignable to type 'string'.
ERROR: 1:138 expect TypeScript@next compile error:
Type 'P' cannot be used to index type 'T'.
```

While the expected behavior of Omit is clearly broken in current @next, someone willing to fix it must be aware that as I've shown in my previous comment, it also have some weird inconsistent behavior in 2.6.2.

@aleksey-bykov Fix is up at #21156.
@aczekajski We are aware of the weird behaviour, and it results from a few different not-quite-sound systems interacting. I consider these types "use at your own risk".

I think the Spread type in #21316 is essentially a superior version of Overwrite here.

Commenting here for folks who got to this issue page from the net after the release of 2.8.

With TS 2.8 the more convenient way of implementing Omit now the below, taking advantage of conditional types and new built-in type Exclude:

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

Along those lines, here's an implementation of Overwrite:

type Overwrite<T1, T2> = {
    [P in Exclude<keyof T1, keyof T2>]: T1[P]
} & T2;

Or, once you have Omit,

type Overwrite<T, U> = Omit<T, Extract<keyof T, keyof U>> & U;

Since there is a new, better Omit, I hoped to do something that seemed impossible with previous TS versions. Let's say I have interfaces like that:

interface P1 {
  type: 1,
  a: string;
}

interface P2 {
  type: 2,
  b: string;
}

type P = { id: number } & (P1 | P2);

What I want to achieve is to get rid of id field and still have a discriminated union. However, when I do Omit<P, 'id'> what I get is no longer a discriminated union, fields a and b disappear during the Exclude phase.

What is interesting, simply rewriting a type P with something like that:

type Rewrite<T> { [K in keyof T]: T[K] };

won't break the union. So I thought about using conditional types to omit the "Exclude" part. So I've written this:

type OmitFromUnion<T, X extends keyof T> = { [K in keyof T]: (K extends X ? never : T[K]) };

Logic in my mind behind this: if rewriting can maintain discriminated union than let's do this but if this particular K is what we want to omit, make it never (instead of excluding this key in the mapping part). This failed miserably, doing effectively nothing (producing discriminated union but still with this id fieldn in it).

Might it be still impossible until there is a method to get all possible keys of discriminated union (for all cases)?

@aczekajski If you want Omit to distribute over unions, you can define it like this:

type Omit<T, K extends keyof T> = T extends any ? Pick<T, Exclude<keyof T, K>> : never

which uses the fact that conditional types automatically distribute the checked type over a union if the checked type is a bare type parameter (T in the above case).

You can verify that the above Omit<P, 'id'> behaves like P1 | P2.

@jcalz Ok, when reading the docs I totally missed the fact that it automatically distributes over a unions. At first glympse, this T extends any seems like it is always true and as such does nothing, so I don't really like the "hackery feeling" about this solution, but old Diff was even more of a hackery.

Summarizing, isn't the omit stated by above comment the better one than the previous solution which brakes unions?

By the way, it's now possible to get all keys for all discriminated union cases than:

type AllPossibleKeys<T> = T extends any ? keyof T : never;

Is there a way with 2.8 Exclude etc to prevent optional properties being spat out as type | undefined here?

type Overwrite<T1, T2> = { [P in Exclude<keyof T1, keyof T2>]: T1[P] } & T2;
type A = Overwrite<{ myOptional?: string, myRequired: number }, { myRequired: string }>;

// compiler complains Property 'myOptional' is missing in type '{ myRequired: string; }' 
let x: A = {
  myRequired: 'hello'
};

@drew-r Can't you just use NonNullable

@drew-r Use this for Overwrite:

type Overwrite<T1, T2> = Pick<T1, Exclude<keyof T1, keyof T2>> & T2

Pick is specially treated by the compiler and will preserve modifiers in the original type, instead of transforming them, like homomorphic mapped types.

@ferdaber, thanks for that interesting bit o' trivia

Pick is specially treated by the compiler and will preserve modifiers in the original type, instead of transforming them, like homomorphic mapped types.

More information can be found in the docs:

Readonly, Partial and Pick are homomorphic whereas Record is not. One clue that Record is not homomorphic is that it doesn’t take an input type to copy properties from:

type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>

Non-homomorphic types are essentially creating new properties, so they can’t copy property modifiers from anywhere.

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

I know this was dropped from the TypeScript codebase, but I see myself and colleagues need this nearly weekly and in every new project. We just use it _so much_ and because it is like the "opposite" of Pick I just feel more and more that this should ship with TypeScript by default. It is especially hard for new TS developers who see Pick and look for Omit and don't now about this GitHub thread.

Is there a way to remove a wide type from a union type without also removing its subtypes?

export interface JSONSchema4 {
  id?: string
  $ref?: string
  // to allow third party extensions
  [k: string]: any
}
type KnownProperties = Exclude<keyof JSONSchema4, string | number>
// I want to end up with
type KnownProperties = 'id' | 'ref'
// But, somewhat understandably, get this
type KnownProperties = never
// yet it seem so very within reach
type Keys = keyof JSONSchema4 // string | number | 'id' | 'ref'

Also on stackoverflow.

Try this:

type KnownKeys<T> = {
    [K in keyof T]: string extends K ? never : number extends K ? never : K
} extends {[_ in keyof T]: infer U} ? U : never;

image
I'm on it, captain!

@ferdaber, it absolutely worked, you are a genius!
Is that somewhat based on the original Diff hack?
I didn't even think to try conditional types here.
I see it's based on the fact that string extends string (just as 'a' extends string) but string doesn't extend 'a', and similarly for numbers.

First it creates a mapped type, where for every key of T, the value is:

  • if string extends key (key is string, not a subtype) => never
  • if number extends key (key is number, not a subtype) => never
  • else, the actual string key

Then, it does essentially valueof to get a union of all the values:

type ValuesOf<T> = T extends { [_ in keyof T]: infer U } ? U : never

```ts
interface test {
req: string
opt: string
}
type FirstHalf = {
}

type ValuesOf = T extends { [_ in keyof T]: infer U } ? U : never
// or equivalently, since T here, and T in FirstHalf have the same keys,
// we can use T from FirstHalf instead:
type SecondHalf = First extends { [_ in keyof T]: infer U } ? U : never;

type a = FirstHalf
//Output:
type a = {
[x: string]: never;
req: "req";
opt?: "opt" | undefined;
}
type a2 = ValuesOf // "req" | "opt" // Success!
type a2b = SecondHalf // "req" | "opt" // Success!

// Substituting, to create a single type definition, we get @ferdaber's solution:
type KnownKeys = {
} extends { [_ in keyof T]: infer U } ? U : never;
// type b = KnownKeys // "req" | "opt" // Absolutely glorious!

@ferdaber That is amazing. The trick is in how infer works... it apparently iterates through all the keys, both "known" (I'd call that "literal") keys and index keys, and then gives the union of the results. That differs from doing T[keyof T] which only ends up extracting the index signature. Very good work.

@qm3ster. you can indeed distinguish optional keys from required keys whose values may be undefined:

type RequiredKnownKeys<T> = {
    [K in keyof T]: {} extends Pick<T, K> ? never : K
} extends { [_ in keyof T]: infer U } ? ({} extends U ? never : U) : never

type OptionalKnownKeys<T> = {
    [K in keyof T]: string extends K ? never : number extends K ? never : {} extends Pick<T, K> ? K : never
} extends { [_ in keyof T]: infer U } ? ({} extends U ? never : U) : never

which produces

type c = RequiredKnownKeys<test> // 'reqButUndefined' | 'req'
type d = OptionalKnownKeys<test> // 'opt'

@jcalz

{} extends Pick<T, K>

Why, I'd never!
Y'all a bunch of hWizards in here or something?

@ferdaber too late, I credited you on StackOverflow and now they'll come get you.
No good deed goes unpunished.

Like @donaldpipowitch indicates above, can we please have Omit in TypeScript as well. The current helper types like Exclude and Pick are super useful. I think Omit is also something that comes in very handy. We can always create it ourselves with

type Omit = Pick>

But having it built-in instead of always having to lookup this type would be super nice! I can always open a new issue to discuss this further.

The Omit helper type has official support as of TypeScript 3.5
https://devblogs.microsoft.com/typescript/announcing-typescript-3-5/#the-omit-helper-type

Was this page helpful?
0 / 5 - 0 ratings