GeezerVison is showing github.com — Conditional types by ahejlsberg · Pull Request #21316 · microsoft/TypeScript
Conversation
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:
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.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
ajafff, azhukaudev, joewood, Bnaya, astashov, olaf89, ErikSchierboom, Shinigami92, shairez, piotrwitek, and 271 more reacted with thumbs up emoji
jonsterling reacted with thumbs down emoji
Bnaya, olaf89, Shinigami92, shairez, piotrwitek, thgreasi, karol-majewski, houfio, j-oliveras, pelotom, and 111 more reacted with hooray emoji
jonsterling, browsermage, and nagarsuresh reacted with confused emoji
mohsen1, exyi, s-ve, olaf89, Shinigami92, shairez, piotrwitek, karol-majewski, thgreasi, alexrock, and 134 more reacted with heart emoji
All reactions
-
👍
281 reactions
-
👎
1 reaction
-
🎉
121 reactions
-
😕
3 reactions
-
❤️
144 reactions
# 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
|
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);

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);

|
niieani, donaldpipowitch, AriaMinaei, KiaraGrouwstra, greghornby, wand3r, and DCoderLT reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
sirian
commented
Mar 28, 2018
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
Leon Adler (@leonadler)
numArgs((...args: any[]) => {}); // 10
numArgs((a: string, ...args: any[]) => {}); //10
numArgs((a: object) => {}); // 0

|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
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
|
gcnew and leonadler reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
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"
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
sirian
commented
Mar 29, 2018
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
Leon Adler (@leonadler) It was just a notice, that IsValidArgs and NumberOfArgs is not correct (to prevent other users from using incorrect code).
Ryan Cavanaugh (@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
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Leon Adler (@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;
|
sandangel reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
sirian
commented
Apr 12, 2018
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
Anders Hejlsberg (@ahejlsberg) I think many users will copy types from your examples from #21316 (comment).
So some notices about type TypeName<T>
- you missed
symbol
typeof Object === "function" but TypeName<Object> === "object"
typeof class A{} === "function" but TypeName<A> === "object"
|
izatop reacted with thumbs up emoji
leonadler reacted with thumbs down emoji
All reactions
-
👍
1 reaction
-
👎
1 reaction
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
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"
|
SlurpTheo reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
type Boxed = T extends any[] ? BoxedArray<T[number]> : BoxedValue;
How is it possible to do following?
T extends R[] ? BoxedArray<R> : BoxedValue<T>
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Po Chen (@princemaple) thanks it worked. Actually BoxedArray<T[number]> worked as well but such syntax wasn't obvious to me
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Reviewers
+2 more reviewers
Reviewers whose approvals may not affect merge requirements
Uh oh!
There was an error while loading. Please reload this page.
Assignees
No one assigned
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
|