Skip to content

Conversation

@ahejlsberg
Copy link
Member

@ahejlsbergahejlsberg 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:

TextendsU ? 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:

typeTypeName<T>=Textendsstring ? "string" : Textendsnumber ? "number" : Textendsboolean ? "boolean" : Textendsundefined ? "undefined" : TextendsFunction ? "function" : "object";typeT0=TypeName<string>;// "string"typeT1=TypeName<"a">;// "string"typeT2=TypeName<true>;// "boolean"typeT3=TypeName<()=>void>;// "function"typeT4=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).

typeT10=TypeName<string|(()=>void)>;// "string" | "function"typeT12=TypeName<string|string[]|undefined>;// "string" | "object" | "undefined"typeT11=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).

typeBoxedValue<T>={value: T};typeBoxedArray<T>={array: T[]};typeBoxed<T>=Textendsany[] ? BoxedArray<T[number]> : BoxedValue<T>;typeT20=Boxed<string>;// BoxedValue<string>typeT21=Boxed<number[]>;// BoxedArray<number>typeT22=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:

typeDiff<T,U>=TextendsU ? never : T;// Remove types from T that are assignable to UtypeFilter<T,U>=TextendsU ? T : never;// Remove types from T that are not assignable to UtypeT30=Diff<"a"|"b"|"c"|"d","a"|"c"|"f">;// "b" | "d"typeT31=Filter<"a"|"b"|"c"|"d","a"|"c"|"f">;// "a" | "c"typeT32=Diff<string|number|(()=>void),Function>;// string | numbertypeT33=Filter<string|number|(()=>void),Function>;// () => voidtypeNonNullable<T>=Diff<T,null|undefined>;// Remove null and undefined from TtypeT34=NonNullable<string|number|undefined>;// string | numbertypeT35=NonNullable<string|string[]|null|undefined>;// string | string[]functionf1<T>(x: T,y: NonNullable<T>){x=y;// Oky=x;// Error}functionf2<Textendsstring|undefined>(x: T,y: NonNullable<T>){x=y;// Oky=x;// Errorlets1: string=x;// Errorlets2: string=y;// Ok}

Conditional types are particularly useful when combined with mapped types:

typeFunctionPropertyNames<T>={[KinkeyofT]: T[K]extendsFunction ? K : never}[keyofT];typeFunctionProperties<T>=Pick<T,FunctionPropertyNames<T>>;typeNonFunctionPropertyNames<T>={[KinkeyofT]: T[K]extendsFunction ? never : K}[keyofT];typeNonFunctionProperties<T>=Pick<T,NonFunctionPropertyNames<T>>;interfacePart{id: number;name: string;subparts: Part[];updatePart(newName: string): void;}typeT40=FunctionPropertyNames<Part>;// "updatePart"typeT41=NonFunctionPropertyNames<Part>;// "id" | "name" | "subparts"typeT42=FunctionProperties<Part>;//{updatePart(newName: string): void }typeT43=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):

typeDeepReadonly<T>=Textendsany[] ? DeepReadonlyArray<T[number]> : Textendsobject ? DeepReadonlyObject<T> : T;interfaceDeepReadonlyArray<T>extendsReadonlyArray<DeepReadonly<T>>{}typeDeepReadonlyObject<T>={readonly[PinNonFunctionPropertyNames<T>]: DeepReadonly<T[P]>;};functionf10(part: DeepReadonly<Part>){letname: string=part.name;letid: number=part.subparts[0].id;part.id=part.id;// Errorpart.subparts[0]=part.subparts[0];// Errorpart.subparts[0].id=part.subparts[0].id;// Errorpart.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:

typeElementType<T>=Textendsany[] ? 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!

typeIsValidArg<T>=Textendsobject ? keyofTextendsnever ? false : true : true;typeNumberOfArgs<TextendsFunction>=Textends(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>extendstrue ? 10 : IsValidArg<I>extendstrue ? 9 : IsValidArg<H>extendstrue ? 8 : IsValidArg<G>extendstrue ? 7 : IsValidArg<F>extendstrue ? 6 : IsValidArg<E>extendstrue ? 5 : IsValidArg<D>extendstrue ? 4 : IsValidArg<C>extendstrue ? 3 : IsValidArg<B>extendstrue ? 2 : IsValidArg<A>extendstrue ? 1 : 0) : 0;functionnumArgs<TextendsFunction>(fn: T): NumberOfArgs<T>{returnfn.lengthasany;}declarefunctionexampleFunction(a: number,b: string,c?: any[]): void;consttest=numArgs(exampleFunction);

screenshot


typePromisified<TextendsFunction>=Textends(...args: any[])=>Promise<any> ? T : (Textends(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>extendstrue ? (a: A,b: B,c: C,d: D,e: E,f: F,g: G,h: H,i: I,j: J)=>Promise<R> : IsValidArg<I>extendstrue ? (a: A,b: B,c: C,d: D,e: E,f: F,g: G,h: H,i: I)=>Promise<R> : IsValidArg<H>extendstrue ? (a: A,b: B,c: C,d: D,e: E,f: F,g: G,h: H)=>Promise<R> : IsValidArg<G>extendstrue ? (a: A,b: B,c: C,d: D,e: E,f: F,g: G)=>Promise<R> : IsValidArg<F>extendstrue ? (a: A,b: B,c: C,d: D,e: E,f: F)=>Promise<R> : IsValidArg<E>extendstrue ? (a: A,b: B,c: C,d: D,e: E)=>Promise<R> : IsValidArg<D>extendstrue ? (a: A,b: B,c: C,d: D)=>Promise<R> : IsValidArg<C>extendstrue ? (a: A,b: B,c: C)=>Promise<R> : IsValidArg<B>extendstrue ? (a: A,b: B)=>Promise<R> : IsValidArg<A>extendstrue ? (a: A)=>Promise<R> : ()=>Promise<R>) : never);declarefunctionpromisify<TextendsFunction>(fn: T): Promisified<T>;declarefunctionexampleFunction2(a: number,b: string,c?: any[]): RegExp;consttest2=promisify(exampleFunction2);

screenshot

@sirian
Copy link
Contributor

#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

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

typeofObject==="function"TypeName<typeofObject>==="function"typeofObject.create(Object.prototype)==="object"TypeName<Object>==="object"
classA{}typeofA==="function"TypeName<typeofA>==="function"typeof(newA())==="object"TypeName<A>==="object"

@KiaraGrouwstraKiaraGrouwstra mentioned this pull request Apr 19, 2018
@pleerock
Copy link

pleerock commented May 10, 2018

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

How is it possible to do following?

TextendsR[] ? 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 freeto 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

20 participants

@ahejlsberg@DanielRosenwasser@lyrachord@Retsam@dead-claudia@AlexGalays@jack-williams@AriaMinaei@neoncom@treybrisbane@falsandtru@mbrodersen@sandersn@vendethiel@mohsen1@KiaraGrouwstra@jcalz@pelotom@zpdDG4gta8XKpMCd@MartinJohns