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

Conditional types #21316

Merged
merged 44 commits into from Feb 3, 2018

Conversation

Projects
None yet
@ahejlsberg
Member

ahejlsberg commented Jan 20, 2018

This PR introduces conditional types which add the ability to express non-uniform type mappings. A conditional type selects one of two possible types based on a condition expressed as a type relationship test:

T extends U ? X : Y

The type above means when T is assignable to U the type is X, otherwise the type is Y. Evaluation of a conditional type is deferred when evaluation of the condition depends on type variables in T or U, but is resolved to either X or Y when the condition depends on no type variables.

An example:

type TypeName<T> =
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";

type T0 = TypeName<string>;  // "string"
type T1 = TypeName<"a">;  // "string"
type T2 = TypeName<true>;  // "boolean"
type T3 = TypeName<() => void>;  // "function"
type T4 = TypeName<string[]>;  // "object"

Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation. For example, an instantiation of T extends U ? X : Y with the type argument A | B | C for T is resolved as (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y).

type T10 = TypeName<string | (() => void)>;  // "string" | "function"
type T12 = TypeName<string | string[] | undefined>;  // "string" | "object" | "undefined"
type T11 = TypeName<string[] | number[]>;  // "object"

In instantiations of a distributive conditional type T extends U ? X : Y, references to T within the conditional type are resolved to individual constituents of the union type (i.e. T refers to the individual constituents after the conditional type is distributed over the union type). Furthermore, references to T within X have an additional type parameter constraint U (i.e. T is considered assignable to U within X).

type BoxedValue<T> = { value: T };
type BoxedArray<T> = { array: T[] };
type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>;

type T20 = Boxed<string>;  // BoxedValue<string>;
type T21 = Boxed<number[]>;  // BoxedArray<number>;
type T22 = Boxed<string | number[]>;  // BoxedValue<string> | BoxedArray<number>;

Notice that T has the additional constraint any[] within the true branch of Boxed<T> and it is therefore possible to refer to the element type of the array as T[number]. Also, notice how the conditional type is distributed over the union type in the last example.

The distributive property of conditional types can conveniently be used to filter union types:

type Diff<T, U> = T extends U ? never : T;  // Remove types from T that are assignable to U
type Filter<T, U> = T extends U ? T : never;  // Remove types from T that are not assignable to U

type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"
type T32 = Diff<string | number | (() => void), Function>;  // string | number
type T33 = Filter<string | number | (() => void), Function>;  // () => void

type NonNullable<T> = Diff<T, null | undefined>;  // Remove null and undefined from T

type T34 = NonNullable<string | number | undefined>;  // string | number
type T35 = NonNullable<string | string[] | null | undefined>;  // string | string[]

function f1<T>(x: T, y: NonNullable<T>) {
    x = y;  // Ok
    y = x;  // Error
}

function f2<T extends string | undefined>(x: T, y: NonNullable<T>) {
    x = y;  // Ok
    y = x;  // Error
    let s1: string = x;  // Error
    let s2: string = y;  // Ok
}

Conditional types are particularly useful when combined with mapped types:

type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;

type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;

interface Part {
    id: number;
    name: string;
    subparts: Part[];
    updatePart(newName: string): void;
}

type T40 = FunctionPropertyNames<Part>;  // "updatePart"
type T41 = NonFunctionPropertyNames<Part>;  // "id" | "name" | "subparts"
type T42 = FunctionProperties<Part>;  // { updatePart(newName: string): void }
type T43 = NonFunctionProperties<Part>;  // { id: number, name: string, subparts: Part[] }

Combining all of the above to create a DeepReadonly<T> type that recursively makes all properties of an object read-only and removes all function properties (i.e. methods):

type DeepReadonly<T> =
    T extends any[] ? DeepReadonlyArray<T[number]> :
    T extends object ? DeepReadonlyObject<T> :
    T;

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

type DeepReadonlyObject<T> = {
    readonly [P in NonFunctionPropertyNames<T>]: DeepReadonly<T[P]>;
};

function f10(part: DeepReadonly<Part>) {
    let name: string = part.name;
    let id: number = part.subparts[0].id;
    part.id = part.id;  // Error
    part.subparts[0] = part.subparts[0];  // Error
    part.subparts[0].id = part.subparts[0].id;  // Error
    part.updatePart("hello");  // Error
}

Similar to union and intersection types, conditional types are not permitted to reference themselves recursively (however, indirect references through interface types or object literal types are allowed, as illustrated by the DeepReadonly<T> example above). For example the following is an error:

type ElementType<T> = T extends any[] ? ElementType<T[number]> : T;  // Error

For further examples see the tests associated with the PR.

EDIT: See #21496 for type inference in conditional types.

Fixes #12215.
Fixes #12424.

ahejlsberg added some commits Dec 5, 2017

Merge branch 'master' into conditionalTypes
# Conflicts:
#	src/compiler/checker.ts
Merge branch 'master' into conditionalTypes
# Conflicts:
#	src/compiler/checker.ts
#	src/compiler/types.ts
#	tests/baselines/reference/api/tsserverlibrary.d.ts
#	tests/baselines/reference/api/typescript.d.ts
@leonadler

This comment has been minimized.

leonadler commented Mar 19, 2018

Sorry if there is a more appropriate place to post this, but thanks for the new (albeit sometimes complicated) ways to express behavior!

type IsValidArg<T> = T extends object ? keyof T extends never ? false : true : true;

type NumberOfArgs<T extends Function> = 
    T extends (a: infer A, b: infer B, c: infer C, d: infer D, e: infer E, f: infer F, g: infer G, h: infer H, i: infer I, j: infer J) => any ? (
        IsValidArg<J> extends true ? 10 :
        IsValidArg<I> extends true ? 9 :
        IsValidArg<H> extends true ? 8 :
        IsValidArg<G> extends true ? 7 :
        IsValidArg<F> extends true ? 6 :
        IsValidArg<E> extends true ? 5 :
        IsValidArg<D> extends true ? 4 :
        IsValidArg<C> extends true ? 3 :
        IsValidArg<B> extends true ? 2 :
        IsValidArg<A> extends true ? 1 : 0
    ) : 0;

function numArgs<T extends Function>(fn: T): NumberOfArgs<T> {
    return fn.length as any;
}
    
declare function exampleFunction(a: number, b: string, c?: any[]): void;
const test = numArgs(exampleFunction);

screenshot


type Promisified<T extends Function> =
    T extends (...args: any[]) => Promise<any> ? T : (
        T extends (a: infer A, b: infer B, c: infer C, d: infer D, e: infer E, f: infer F, g: infer G, h: infer H, i: infer I, j: infer J) => infer R ? (
            IsValidArg<J> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J) => Promise<R> :
            IsValidArg<I> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I) => Promise<R> :
            IsValidArg<H> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H) => Promise<R> :
            IsValidArg<G> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G) => Promise<R> :
            IsValidArg<F> extends true ? (a: A, b: B, c: C, d: D, e: E, f: F) => Promise<R> :
            IsValidArg<E> extends true ? (a: A, b: B, c: C, d: D, e: E) => Promise<R> :
            IsValidArg<D> extends true ? (a: A, b: B, c: C, d: D) => Promise<R> :
            IsValidArg<C> extends true ? (a: A, b: B, c: C) => Promise<R> :
            IsValidArg<B> extends true ? (a: A, b: B) => Promise<R> :
            IsValidArg<A> extends true ? (a: A) => Promise<R> :
            () => Promise<R>
        ) : never
    );

declare function promisify<T extends Function>(fn: T): Promisified<T>;

declare function exampleFunction2(a: number, b: string, c?: any[]): RegExp;

const test2 = promisify(exampleFunction2);

screenshot

@sirian

This comment has been minimized.

sirian commented Mar 27, 2018

@sirian

This comment has been minimized.

sirian commented Mar 28, 2018

@leonadler

numArgs((...args: any[]) => {});  // 10
numArgs((a: string, ...args: any[]) => {}); //10
numArgs((a: object) => {}); // 0

image

@RyanCavanaugh

This comment has been minimized.

Member

RyanCavanaugh commented Mar 28, 2018

@sirian please post code-fenced blocks instead of screenshots; no one wants to have to type in a dozen lines of code to add comments or observe behavior

@leonadler

This comment has been minimized.

leonadler commented Mar 29, 2018

@sirian are you asking a specific question?

My example was not meant to be feature-complete, just a result of experimenting with 2.8 for a few minutes. It depends on typescripts infer keyword inferring unprovided parameter types as {}:

type TypeOfFirstArg<T extends Function> = T extends (a: infer FirstArg) => any ? FirstArg : never;

function functionWithOneParameter(a: number) { }
let exampleA: TypeOfFirstArg<typeof functionWithOneParameter>;
// exampleA is of type "number", as you would expect

function functionWithNoParameter() { }
let exampleB: TypeOfFirstArg<typeof functionWithNoParameter>;
// exampleB is "{}", although I would have expected "never"
@sirian

This comment has been minimized.

sirian commented Mar 29, 2018

@leonadler It was just a notice, that IsValidArgs and NumberOfArgs is not correct (to prevent other users from using incorrect code).

@RyanCavanaugh Mi fault, I was sure I attached code.

type IsValidArg<T> = T extends object ? keyof T extends never ? false : true : true;

type Valid<T> = IsValidArg<T> extends true ? T : never

declare function isValid<T extends Valid<V>, V = T>(value: T):IsValidArg<T>;

isValid(3); // ok

isValid({}); // wrong, false === IsValidArg
isValid({} as Record<string, any>); // ok
isValid({} as {x?: any}); // ok

isValid(Function); //ok
isValid(class Foo{}); // ok
isValid(new class Foo{}); //wrong, false === IsValidArg
isValid(() => 1); // wrong, false === IsValidArg

isValid(0 as never); // maybe wrong, true === IsValidArg
@sirian

This comment has been minimized.

sirian commented Mar 29, 2018

@leonadler I have an idea! We could use reverse extends check. Look at scratch:

type NumOfArgs<F extends Function> = F extends (a: infer A, b: infer B, c: infer C, ...args: (infer Z)[]) => infer R ? (
    ((a: A, b: B, c: C, ...args: Z[]) => R) extends F ? number :
    ((a: A, b: B, c: C) => R) extends F ? 3 :
    ((a: A, b: B) => R) extends F ? 2 :
    ((a: A) => R) extends F ? 1 :
    0
) : never;
@sirian

This comment has been minimized.

sirian commented Apr 12, 2018

@ahejlsberg I think many users will copy types from your examples from #21316 (comment).
So some notices about type TypeName<T>

  1. you missed symbol
  2. typeof Object === "function" but TypeName<Object> === "object"
  3. typeof class A{} === "function" but TypeName<A> === "object"
@leonadler

This comment has been minimized.

leonadler commented Apr 13, 2018

@sirian You seem to confuse "type A" with "object of type A".
The TypeName of an object with the type Object is "object".
Calling typeof Object retrieves the type of the constructor, which is "function".
When you call typeof a with an a that has the type Object, you will get "object", as expected.

Similarily, when you write let a: MyClass in TypeScript, it means a is an instance of the type MyClass, not a has the same type that the function MyClass has.

typeof Object === "function"
TypeName<typeof Object> === "function"
typeof Object.create(Object.prototype) === "object"
TypeName<Object> === "object"
class A { }
typeof A === "function"
TypeName<typeof A> === "function"
typeof (new A()) === "object"
TypeName<A> === "object"

@tycho01 tycho01 referenced this pull request Apr 19, 2018

Closed

Subtraction types #4183

@ericanderson ericanderson referenced this pull request May 8, 2018

Open

Adds DeepReadonly #17

@pleerock

This comment has been minimized.

pleerock commented May 10, 2018

type Boxed = T extends any[] ? BoxedArray<T[number]> : BoxedValue;

How is it possible to do following?

T extends R[] ? BoxedArray<R> : BoxedValue<T>
@princemaple

This comment has been minimized.

princemaple commented May 10, 2018

@pleerock T extends (infer R)[]

@pleerock

This comment has been minimized.

pleerock commented May 10, 2018

@princemaple thanks it worked. Actually BoxedArray<T[number]> worked as well but such syntax wasn't obvious to me

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.