Skip to content

Provide "remove from union" type operator #12959

@PyroVortex

Description

@PyroVortex

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions