-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
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 Edit: corrected that |
this is not accurate. |
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 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 Once #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:
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. |
#13470 is not the same, since it does not allow you to dynamically create difference types. |
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 // 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 |
@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. 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 Things like converting those back to tuple types, or just straight type-level Edit: |
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 |
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' |
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; |
@ferdaber, it absolutely worked, you are a genius! First it creates a mapped type, where for every key of T, the value is:
Then, it does essentially 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! |
@ferdaber That is amazing. The trick is in how @qm3ster. you can indeed distinguish optional keys from required keys whose values may be 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' |
All credit goes to @ajafff! #25987 (comment) |
Like @donaldpipowitch indicates above, can we please have
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 |
Can anyone help me try to get keys that I can work with from the following:
How do I get something like 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 |
@mattvb91 types don't exist at runtime. There's no difference in JS between an optional and a required field. This is not the place for that kind of question though, you should go to a support place, such as |
Note that the 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]; }; |
Now we have Mapped Types and keyof, we could add a subtraction operator that only works on literal types:
The text was updated successfully, but these errors were encountered: