-
Notifications
You must be signed in to change notification settings - Fork 13k
Closed
Labels
DuplicateAn existing issue was already createdAn existing issue was already created
Description
With the introduction of the keyof
operator and mapped types, the ability to remove some subset of the types in a union becomes increasingly useful. The type system already supports this operation via type guards in control flow, but we have no way to describe the resulting type to the compiler.
Available existing behavior
type Direction = 'north' | 'east' | 'south' | 'west';
type EastWest = 'east' | 'west';
declare function is<T>(x: any): x is T;
let dir: Direction; // dir has type 'north' | 'east' | 'south' | 'west';
let nsDir = !is<EastWest>(dir) && dir; // nsDir has type 'north' | 'south'
// because the compiler removed 'east' | 'west' from the type of dir via the type guard
However, we have no way of specifying the type of nsDir
in terms of Direction
and EastWest
Proposal
We introduce a new "remove from union" operator A -| B
(symbol choice subject to change) that has the following behavior:
// Simple types
type example1 = (number | string) -| number; // string
// number was removed from the union
type example2 = (number | string) -| (string | boolean); // number
// string was removed, boolean was ignored as no overlap with number | string
type example3 = number -| string; // number
// string was ignored as no overlap with number.
// This may need to be revisited with regards to indexers
// since [key: string] is considered a superset of [key: number]
type example4 = number -| number; // never
// Union types
type A = 'a' | 'b' | 1 | 2;
type B = string | number; // union of all string literal types and all numeric literal types
type C = 'a' | 2;
type example5 = A -| B; // never
type example6 = B -| A; // This may need to be defined as "any string except 'a' or 'b',
// and any number except '1' or '2'", which we currently can't represent.
// In the near term, B will suffice as the result
type example7 = A -| C; // 'b' | '1'
// Interfaces
interface IFoo {
a: number;
}
interface IBar {
a: number;
b: string;
}
interface IBaz {
a: number;
}
type example8 = IFoo -| IBar; // IFoo
type example9 = IFoo -| IBaz; // never, based on the existing behavior of type guards
type example10 = (IFoo | IBar) -| IFoo; // IBar
This will also allow us to solve some current proposals:
// Object rest types
type Rest<T, K extends keyof T> = {[P in (keyof T) -| K]: T[P]};
// Object spread types
type Assign<A, B> = B & Rest<A, (keyof A) & (keyof B)>;
// Note that (keyof A) & (keyof B) is identical to keyof (A | B)
// Wrapping a function and providing some inputs (really just object spread and rest types)
function provideA<PropsType, K extends keyof PropsType>(
f: (props: PropsType) => void,
provided: {[P in K]: PropsType[P]}): (remainingProps: Rest<PropsType, K>) => void {
return (remainingProps: Rest<PropsType, K>): void => {
return f({...remainingProps, ...provided});
};
}
Edit: fixed variables in provideA
, spacing
@sandersn, @mhegazy
beenotung, dawidcxx and wildeyes
Metadata
Metadata
Assignees
Labels
DuplicateAn existing issue was already createdAn existing issue was already created