New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for literal type subtraction #12215

Closed
cvle opened this Issue Nov 14, 2016 · 78 comments

Comments

Projects
None yet
@cvle

cvle commented Nov 14, 2016

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

This comment has been minimized.

Show comment
Hide comment
@aluanhaddad

aluanhaddad Nov 14, 2016

Contributor

Duplicate of #4183?

Contributor

aluanhaddad commented Nov 14, 2016

Duplicate of #4183?

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Nov 15, 2016

Member

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

Member

ahejlsberg commented Nov 15, 2016

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

@aleksey-bykov

This comment has been minimized.

Show comment
Hide comment
@aleksey-bykov

aleksey-bykov Nov 17, 2016

@ahejlsberg checkout my proposal at #4183

@ahejlsberg checkout my proposal at #4183

@whitecolor

This comment has been minimized.

Show comment
Hide comment
@whitecolor

whitecolor Nov 22, 2016

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

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

@PyroVortex

This comment has been minimized.

Show comment
Hide comment
@PyroVortex

PyroVortex Dec 15, 2016

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.

PyroVortex commented Dec 15, 2016

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.

@mhegazy

This comment has been minimized.

Show comment
Hide comment
@mhegazy

mhegazy Dec 16, 2016

Contributor

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.

Contributor

mhegazy commented Dec 16, 2016

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.

@threehams

This comment has been minimized.

Show comment
Hide comment
@threehams

threehams Dec 16, 2016

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.

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.

@niieani

This comment has been minimized.

Show comment
Hide comment
@niieani

niieani Mar 17, 2017

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 #13470 lands, we can do even better.

niieani commented Mar 17, 2017

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 #13470 lands, we can do even better.

@janv

This comment has been minimized.

Show comment
Hide comment
@janv

janv Apr 25, 2017

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

janv commented Apr 25, 2017

@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...'.
@yhaskell

This comment has been minimized.

Show comment
Hide comment
@yhaskell

yhaskell May 9, 2017

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.

yhaskell commented May 9, 2017

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.

@tycho01

This comment has been minimized.

Show comment
Hide comment
@tycho01

tycho01 May 29, 2017

Contributor

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

Contributor

tycho01 commented May 29, 2017

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

@niieani

This comment has been minimized.

Show comment
Hide comment
@niieani

niieani May 29, 2017

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

niieani commented May 29, 2017

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

@tycho01

This comment has been minimized.

Show comment
Hide comment
@tycho01

tycho01 Jun 1, 2017

Contributor

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.

Contributor

tycho01 commented Jun 1, 2017

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.

@tycho01

This comment has been minimized.

Show comment
Hide comment
@tycho01

tycho01 Jun 3, 2017

Contributor

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? 😄

Contributor

tycho01 commented Jun 3, 2017

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? 😄

@niieani

This comment has been minimized.

Show comment
Hide comment
@niieani

niieani Jun 3, 2017

@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 commented Jun 3, 2017

@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! :)

@tycho01

This comment has been minimized.

Show comment
Hide comment
@tycho01

tycho01 Jun 4, 2017

Contributor

@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.

Contributor

tycho01 commented Jun 4, 2017

@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.

@nirendy

This comment has been minimized.

Show comment
Hide comment
@nirendy

nirendy Jun 11, 2017

@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 commented Jun 11, 2017

@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,
}
@niieani

This comment has been minimized.

Show comment
Hide comment
@niieani

niieani Jun 11, 2017

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

niieani commented Jun 11, 2017

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

@tycho01

This comment has been minimized.

Show comment
Hide comment
@tycho01

tycho01 Jun 11, 2017

Contributor

@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?! 😆

Contributor

tycho01 commented Jun 11, 2017

@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?! 😆

@aleksey-bykov

This comment has been minimized.

Show comment
Hide comment
@aleksey-bykov

aleksey-bykov Jun 11, 2017

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

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

@nirendy

This comment has been minimized.

Show comment
Hide comment
@nirendy

nirendy Jun 11, 2017

@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 commented Jun 11, 2017

@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!

@tycho01

This comment has been minimized.

Show comment
Hide comment
@tycho01

tycho01 Jun 11, 2017

Contributor

@nirendy: yeah, no complaints here! 😃

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

Contributor

tycho01 commented Jun 11, 2017

@nirendy: yeah, no complaints here! 😃

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

@aleksey-bykov

This comment has been minimized.

Show comment
Hide comment
@aleksey-bykov

aleksey-bykov Jan 11, 2018

this pr might have broken it: #17912

this pr might have broken it: #17912

@aczekajski

This comment has been minimized.

Show comment
Hide comment
@aczekajski

aczekajski Jan 11, 2018

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.

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.

@sandersn

This comment has been minimized.

Show comment
Hide comment
@sandersn

sandersn Jan 12, 2018

Member

@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".

Member

sandersn commented Jan 12, 2018

@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".

@tycho01

This comment has been minimized.

Show comment
Hide comment
@tycho01

tycho01 Feb 11, 2018

Contributor

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

Contributor

tycho01 commented Feb 11, 2018

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

@ferdaber

This comment has been minimized.

Show comment
Hide comment
@ferdaber

ferdaber Mar 30, 2018

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

ferdaber commented Mar 30, 2018

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>>
@dallonf

This comment has been minimized.

Show comment
Hide comment
@dallonf

dallonf Mar 31, 2018

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

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

dallonf commented Mar 31, 2018

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

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

This comment has been minimized.

Show comment
Hide comment
@pelotom

pelotom Apr 3, 2018

Or, once you have Omit,

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

pelotom commented Apr 3, 2018

Or, once you have Omit,

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

This comment has been minimized.

Show comment
Hide comment
@aczekajski

aczekajski Apr 4, 2018

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)?

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)?

@jcalz

This comment has been minimized.

Show comment
Hide comment
@jcalz

jcalz Apr 4, 2018

Contributor

@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.

Contributor

jcalz commented Apr 4, 2018

@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.

@aczekajski

This comment has been minimized.

Show comment
Hide comment
@aczekajski

aczekajski Apr 5, 2018

@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?

@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?

@aczekajski

This comment has been minimized.

Show comment
Hide comment
@aczekajski

aczekajski Apr 5, 2018

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;

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;
@drew-r

This comment has been minimized.

Show comment
Hide comment
@drew-r

drew-r May 23, 2018

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 commented May 23, 2018

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'
};
@danielbodart

This comment has been minimized.

Show comment
Hide comment
@danielbodart

danielbodart Jun 1, 2018

@drew-r Can't you just use NonNullable<T[P]>?

@drew-r Can't you just use NonNullable<T[P]>?

@tsofist

This comment has been minimized.

Show comment
Hide comment

tsofist commented Jun 1, 2018

@drew-r try this solution: #12215 (comment)

@ferdaber

This comment has been minimized.

Show comment
Hide comment
@ferdaber

ferdaber Jun 1, 2018

@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 commented Jun 1, 2018

@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.

@rozzzly

This comment has been minimized.

Show comment
Hide comment
@rozzzly

rozzzly Jun 2, 2018

@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.

rozzzly commented Jun 2, 2018

@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.

nikeee added a commit to nikeee/dot-language-support that referenced this issue Jun 3, 2018

@ferdaber

This comment has been minimized.

Show comment
Hide comment
@ferdaber

ferdaber Jun 4, 2018

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.

ferdaber commented Jun 4, 2018

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.

@donaldpipowitch

This comment has been minimized.

Show comment
Hide comment
@donaldpipowitch

donaldpipowitch Jul 31, 2018

Contributor

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

I know this was dropped so far from the TypeScript codebase, but I see myself and colleagues 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 be default. It is especially hard for new TS developers who see Pick and look for Omit and don't now about this GitHub thread.

Contributor

donaldpipowitch commented Jul 31, 2018

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

I know this was dropped so far from the TypeScript codebase, but I see myself and colleagues 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 be default. It is especially hard for new TS developers who see Pick and look for Omit and don't now about this GitHub thread.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment