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 · 90 comments · Fixed by #21316
Closed

Add support for literal type subtraction #12215

cvle opened this issue Nov 14, 2016 · 90 comments · Fixed by #21316
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@cvle
Copy link

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 commented Nov 14, 2016

Duplicate of #4183?

@mhegazy mhegazy added the Duplicate An existing issue was already created label Nov 14, 2016
@ahejlsberg ahejlsberg added Suggestion An idea for TypeScript and removed Duplicate An existing issue was already created labels Nov 15, 2016
@ahejlsberg
Copy link
Member

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

@zpdDG4gta8XKpMCd
Copy link

@ahejlsberg checkout my proposal at #4183

@wclr
Copy link

wclr commented Nov 22, 2016

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

@PyroVortex
Copy link

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

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

@RyanCavanaugh RyanCavanaugh added the In Discussion Not yet reached consensus label May 9, 2017
@KiaraGrouwstra
Copy link
Contributor

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

@niieani
Copy link

niieani commented May 29, 2017

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

@KiaraGrouwstra
Copy link
Contributor

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

@KiaraGrouwstra
Copy link
Contributor

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

@KiaraGrouwstra
Copy link
Contributor

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

@donaldpipowitch
Copy link
Contributor

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 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 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 commented Aug 21, 2018

image
I'm on it, captain!

@qm3ster
Copy link

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

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

@qm3ster
Copy link

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

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

@mattvb91
Copy link

mattvb91 commented Mar 4, 2021

Can anyone help me try to get keys that I can work with from the following:

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

How do I get something like Object.keys(d) so I can console.log(d) and it will output 'opt'

So essentially im trying to get the keys that are left over on the type so I can work with them further. I cant use a type as a value so im not sure how I can get the remaining keys out to a workable format

@qm3ster
Copy link

qm3ster commented Mar 4, 2021

@mattvb91 types don't exist at runtime. There's no difference in JS between an optional and a required field.
If you want "type information" at runtime, you need to define it in JS language values, and then maybe you can make TypeScript typecheck something based on that information if it is declared as const so the string names are literal and not just string.

This is not the place for that kind of question though, you should go to a support place, such as
Discord: https://discord.gg/typescript
Gitter: https://gitter.im/Microsoft/TypeScript

@jcalz
Copy link
Contributor

jcalz commented Sep 27, 2021

Note that the implementation of KnownKeys in this thread has been broken since TypeScript 4.3. See #44143 for details and a workaround implementation of

// TS4.2+ version
type KnownKeys<T> = keyof { [P in keyof T as
    string extends P ? never : number extends P ? never : P
    ]: T[P]; };

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.