GeezerVison is showing github.com — Type inference in conditional types by ahejlsberg · Pull Request #21496 · microsoft/TypeScript
Conversation
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;
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
ajafff, yortus, jwbay, TylorS, nevir, pelotom, yuit, cartant, tvald, HerringtonDarkholme, and 56 more reacted with thumbs up emoji
RyanCavanaugh, phiresky, piotrwitek, Retsam, niieani, kimjoar, jwbay, TylorS, nevir, yuit, and 46 more reacted with hooray emoji
piotrwitek, alexrock, dsifford, niieani, jwbay, TylorS, nevir, yuit, treybrisbane, HerringtonDarkholme, and 48 more reacted with heart emoji
All reactions
-
👍
66 reactions
-
🎉
56 reactions
-
❤️
58 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
|
Simon Buchan (@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.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
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();
|
piotrwitek reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Feb 8, 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!
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
grahamc reacted with hooray emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Joe Calzaretta (@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.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
@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.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
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?
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Meirion Hughes (@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).
|
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.
|
Any updates on recursively handling this?
|
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.
|
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?
|
KiaraGrouwstra reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Anders Hejlsberg (@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?
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
As ReturnType was added as a built-in helper are there plans to add Unpacked as well?
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Reviewers
1 more reviewer
Reviewers whose approvals may not affect merge requirements
Uh oh!
There was an error while loading. Please reload this page.
Assignees
No one assigned
Development
Successfully merging this pull request may close these issues.
Uh oh!
There was an error while loading. Please reload this page.
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.
|