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

Type inference in conditional types #21496

Merged
merged 66 commits into from
Feb 3, 2018
Merged

Type inference in conditional types #21496

merged 66 commits into from
Feb 3, 2018

Conversation

ahejlsberg
Copy link
Member

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

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

@simonbuchan You don't need infer for that,

declare function zip<A, B>(a: ReadonlyArray<A>, b: ReadonlyArray<B>): Array<[A, B]>;

You really only need infer when A is part of another type you don't particularly know about, for example, you have T which could be a U[], and you need U.

@simonbuchan
Copy link

The issue is more about how you would describe infer so that users don't expect to be able to do that. (Or maybe decide that alternate syntax is fine and implement it, I dunno.)

Basically: infer T means something like "introduce a new type parameter that is inferred to the contextual type apon instantiation", but that definition is too wide.

Here's another case, where the user is trying to emulate the flow * type:

// Must be a promise, but I can't be bothered writing what kind
let foo: Promise<infer T> = asyncMethod();

@benstevens48
Copy link

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

it's happening!

@KiaraGrouwstra
Copy link
Contributor

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

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

MeirionHughes commented Feb 13, 2018

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

@ericanderson
Copy link
Contributor

Any updates on recursively handling this?

@KiaraGrouwstra
Copy link
Contributor

@MeirionHughes
Copy link

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

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?

benjamn added a commit to benjamn/recast that referenced this pull request Feb 24, 2018
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
@donaldpipowitch
Copy link
Contributor

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

Successfully merging this pull request may close these issues.