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

Type inference in conditional types #21496

Merged
merged 66 commits into from Feb 3, 2018

Conversation

Projects
None yet
@ahejlsberg
Member

ahejlsberg commented Jan 30, 2018

This PR introduces the ability to use type inference in conditional types (#21316), enabling pattern matching for types. For example, the following extracts the return type of a function type:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : T;

Within the extends clause of a conditional type, it is now possible to have infer declarations that introduce a type variable to be inferred. Such inferred type variables may be referenced in the true branch of the conditional type. It is possible to have multiple infer locations for the same type variable.

A conditional type T extends U ? X : Y is either resolved to X or Y, or deferred because the condition depends on one or more type variables. Whether to resolve or defer is determined as follows:

  • First, given types T' and U' that are instantiations of T and U where all occurrences of type parameters are replaced with any, if T' is not assignable to U', the conditional type is resolved to Y. Intuitively, if the most permissive instantiation of T is not assignable to the most permissive instantiation of U, we know that no instantiation will be and we can just resolve to Y.
  • Next, for each type variable introduced by an infer declaration within U collect a set of candidate types by inferring from T to U (using the same inference algorithm as type inference for generic functions). For a given infer type variable V, if any candidates were inferred from co-variant positions, the type inferred for V is a union of those candidates. Otherwise, if any candidates were inferred from contra-variant positions, the type inferred for V is an intersection of those candidates. Otherwise, the type inferred for V is never.
  • Then, given a type T'' that is an instantiation of T where all infer type variables are replaced with the types inferred in the previous step, if T'' is definitely assignable to U, the conditional type is resolved to X. The definitely assignable relation is the same as the regular assignable relation, except that type variable constraints are not considered. Intuitively, when a type is definitely assignable to another type, we know that it will be assignable for all instantiations of those types.
  • Otherwise, the condition depends on one or more type variables and the conditional type is deferred.

Conditional types can be nested to form a sequence of pattern matches that are evaluated in order:

type Unpacked<T> =
    T extends (infer U)[] ? U :
    T extends (...args: any[]) => infer U ? U :
    T extends Promise<infer U> ? U :
    T;

type T0 = Unpacked<string>;  // string
type T1 = Unpacked<string[]>;  // string
type T2 = Unpacked<() => string>;  // string
type T3 = Unpacked<Promise<string>>;  // string
type T4 = Unpacked<Promise<string>[]>;  // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>;  // string

Note that is not possible for a conditional type to recursively reference itself, as might be desired in the Unpacked<T> case above. We're still considering ways in which to implement this.

The following example demonstrates how multiple candidates for the same type variable in co-variant positions causes a union type to be inferred:

type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>;  // string
type T11 = Foo<{ a: string, b: number }>;  // string | number

Likewise, multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred:

type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>;  // string
type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>;  // string & number

When inferring from a type with multiple call signatures (such as the type of an overloaded function), inferences are made from the last signature (which, presumably, is the most permissive catch-all case). It is not possible to perform overload resolution based on a list of argument types (this would require us to support typeof for arbitrary expressions, as suggested in #6606, or something similar).

declare function foo(x: string): number;
declare function foo(x: number): string;
declare function foo(x: string | number): string | number;
type T30 = ReturnType<typeof foo>;  // string | number

It is not possible to use infer declarations in constraint clauses for regular type parameters:

type ReturnType<T extends (...args: any[]) => infer R> = R;  // Error, not supported

However, much the same effect can be obtained by erasing the type variables in the constraint and instead specifying a conditional type:

type AnyFunction = (...args: any[]) => any;
type ReturnType<T extends AnyFunction> = T extends (...args: any[]) => infer R ? R : never;

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

This comment has been minimized.

benstevens48 commented Feb 10, 2018

I actually suggested this more general use of infer (with the syntax =T instead of infer T) some time ago in #13949. It will be interesting to see if this is implemented at some point. The current implementation is already very useful though, thanks!

@masaeedu

This comment has been minimized.

Contributor

masaeedu commented Feb 11, 2018

it's happening!

@tycho01

This comment has been minimized.

Contributor

tycho01 commented Feb 11, 2018

@jcalz: I can't tell what JS function you're typing there, but now that infer is in, I tried to run your type. It doesn't seem to be working yet though. I'm getting this:

type Everything = {
    foo: (x: {
        a: string;
    }) => void;
    bar: (x: {
        b: number;
    }) => void;
    baz: (x: {
        c: boolean;
    }) => void;
}

.. so apparently these Everything instances don't directly have a / b / c.

@jcalz

This comment has been minimized.

Contributor

jcalz commented Feb 11, 2018

@tycho01

Hmm, I just checked 2.8.0-dev.20180211 and it gives:

type Everything = {
    a: string;
} & {
    b: number;
} & {
    c: boolean;
}

which is indeed the intersection type I was aiming for. Not sure why you see something else.

@MeirionHughes

This comment has been minimized.

MeirionHughes commented Feb 13, 2018

a welcome addition; but don't you think ReturnType<T> is confusing? I would expect it to return the actual typescript Type of some function T. Perhaps ReturnPrimitive<T> would allow for a future ReturnType that is a type evaluator can be used for currying? I guess in the future this current ReturnType implementation could be extended though?

@masaeedu

This comment has been minimized.

Contributor

masaeedu commented Feb 13, 2018

@MeirionHughes ReturnType does "return" the return type of a TypeScript function type. Perhaps you're talking about a runtime representation of a type, which at least currently is against the stated goals of the project (see #1573).

@MeirionHughes

This comment has been minimized.

MeirionHughes commented Feb 13, 2018

@masaeedu Yeah, my apologies - I was confusing this one with 21316.

@ericanderson

This comment has been minimized.

ericanderson commented Feb 14, 2018

Any updates on recursively handling this?

@tycho01

This comment has been minimized.

Contributor

tycho01 commented Feb 14, 2018

@MeirionHughes

This comment has been minimized.

MeirionHughes commented Feb 18, 2018

I'm trying to do a promisify and it seems to die if you give it an overloaded method.

The single case works perfectly fine:

type Promisify<T> =
  T extends (cb: (v: infer V) => void) => void ? () => Promise<V> :
  T extends (a: infer A, cb: (v: infer V) => void) => void ? (a: A) => Promise<V> :
  T extends (a: infer A, b: infer B, cb: (v: infer V) => void) => void ? (a: A, b: B) => Promise<V> :
  T;

type Foo = ((a: string, cb: (v) => void) => void)

let bar: Promisify<Foo>;

overload it and it dies completely:

type Foo =
  ((a: string, cb: (v) => void) => void) &
  ((a: string, b: number, cb: (v) => void) => void);

bug? already reported?

never mind - its mentioned it isn't supported - still, it shows nothing (first post says it should be picking the last signature). So maybe it is a bug?

@MeirionHughes

This comment has been minimized.

MeirionHughes commented Feb 23, 2018

@ahejlsberg this limitation on the function overloads is super annoying (not to sound ungrateful: this pr is awesome) - I can't seem to find a way to "mask" the input function (pick from the overloads) either.

You mention you choose the last function from the available ones. Would it not be possible to try each until you arrive at a non-never result?

nicolo-ribaudo added a commit to babel/babel that referenced this pull request Feb 24, 2018

benjamn added a commit to benjamn/recast that referenced this pull request Feb 24, 2018

Implement printer cases for TS{Conditional,Infer}Type.
Although (infer T) must frequently be parenthesized to avoid ambiguity,
Recast does not have to encode that logic into FastPath#needsParens,
because Babylon wraps such types with TSParenthesizedType. Cool!

babel/babel#7404
benjamn/ast-types@da0367b
Microsoft/TypeScript#21316
Microsoft/TypeScript#21496

@kourge kourge referenced this pull request Mar 1, 2018

Open

Joined futures #4

@donaldpipowitch

This comment has been minimized.

Contributor

donaldpipowitch commented May 16, 2018

As ReturnType was added as a built-in helper are there plans to add Unpacked as well?

@Microsoft Microsoft locked and limited conversation to collaborators Jul 31, 2018

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