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

Conditional types #21316

Merged
merged 44 commits into from Feb 3, 2018
Merged

Conditional types #21316

merged 44 commits into from Feb 3, 2018

Conversation

ahejlsberg
Copy link
Member

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

# Conflicts:
#	src/compiler/checker.ts
# Conflicts:
#	src/compiler/checker.ts
#	src/compiler/types.ts
#	tests/baselines/reference/api/tsserverlibrary.d.ts
#	tests/baselines/reference/api/typescript.d.ts
@leonadler
Copy link

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

sirian commented Mar 27, 2018

#22899

@sirian
Copy link
Contributor

sirian commented Mar 28, 2018

@leonadler

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

image

@RyanCavanaugh
Copy link
Member

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

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

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

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

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

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

@pleerock
Copy link

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

@pleerock T extends (infer R)[]

@pleerock
Copy link

@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.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Mapped conditional types Add support for literal type subtraction