Skip to content
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 · 87 comments
Closed

Add support for literal type subtraction #12215

cvle opened this issue Nov 14, 2016 · 87 comments

Comments

@cvle
Copy link

@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
Copy link
Contributor

@aluanhaddad aluanhaddad commented Nov 14, 2016

Duplicate of #4183?

@ahejlsberg
Copy link
Member

@ahejlsberg ahejlsberg commented Nov 15, 2016

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

@zpdDG4gta8XKpMCd
Copy link

@zpdDG4gta8XKpMCd zpdDG4gta8XKpMCd commented Nov 17, 2016

@ahejlsberg checkout my proposal at #4183

@whitecolor
Copy link

@whitecolor whitecolor commented Nov 22, 2016

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

@PyroVortex
Copy link

@PyroVortex 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
Copy link

@mhegazy 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
Copy link

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

@niieani
Copy link

@niieani 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
Copy link

@janv 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
Copy link

@yhaskell 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
Copy link
Contributor

@tycho01 tycho01 commented May 29, 2017

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

@niieani
Copy link

@niieani niieani commented May 29, 2017

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

@tycho01
Copy link
Contributor

@tycho01 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
Copy link
Contributor

@tycho01 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
Copy link

@niieani 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
Copy link
Contributor

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

@ferdaber
Copy link

@ferdaber 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
Copy link

@rozzzly 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
Copy link

@ferdaber 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
Copy link
Contributor

@donaldpipowitch donaldpipowitch commented Jul 31, 2018

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.

@qm3ster
Copy link

@qm3ster qm3ster commented Aug 21, 2018

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.

@ferdaber
Copy link

@ferdaber ferdaber commented Aug 21, 2018

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;
@qm3ster
Copy link

@qm3ster qm3ster commented Aug 21, 2018

image
I'm on it, captain!

@qm3ster
Copy link

@qm3ster qm3ster commented Aug 21, 2018

@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
interface test {
  req: string
  opt: string
  [k: string]: any
}
type FirstHalf<T> = {
  [K in keyof T]: string extends K ? never : number extends K ? never : K
}

type ValuesOf<T> = 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, T> = First extends { [_ in keyof T]: infer U } ? U : never;

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

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

@jcalz jcalz commented Aug 21, 2018

@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'
@ferdaber
Copy link

@ferdaber ferdaber commented Aug 21, 2018

All credit goes to @ajafff! #25987 (comment)

@qm3ster
Copy link

@qm3ster qm3ster commented Aug 21, 2018

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

@SamVerschueren
Copy link

@SamVerschueren SamVerschueren commented Sep 13, 2018

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<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

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.

@rcreasi
Copy link

@rcreasi rcreasi commented May 31, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

You can’t perform that action at this time.