- Notifications
You must be signed in to change notification settings - Fork13.2k
Reference Checker Inference
TypeScript has a number of related techniques which together arecalled type inference: places where a type is discovered frominspecting values instead of a type annotation. This documentcovers them all in one place even though they're all fairly different.
One thing that that is true of all type inference in TypeScript:type inference is a separate step that happens before checking. Thechecker will infer a type for a location; then it will check the typein the normal way, as if the type had been explicitly written. Thisresults in redundant checking when the type inference is simple.
None of these techniques are Hindley-Milner type inference. Instead,TypeScript adds a few ad-hoc inference techniques to its normaltype-checking. The result is a system that can infer from many usefullocations, but nowhere near all of them.
The simplest kind of inference is from initialisers. This inference isso simple that I don't believe it has been given a separate name untilnow.
You can see this anywhere a variable, parameter or property has aninitialiser:
letx=123functionf(x=123){}classC{x=123}
Remember, inference precedes checking, so checkinglet x = 123looks like this:
- Look for the type of
x. - There is no annotation, so use the (widened) type of the initialiser:
number. - Check that the initialiser's type
123is assignable tonumber.
Contextual typing looks upward in the tree for a type based on a typeannotation. This is unlike initialiser inference, which looks at asiblingnode for a type based on avalue. For example, in
constf:Callback=(a,b)=>a.length+b
The parametersa andb are contextually typed by the typeCallback. The checker discovers this by looking at the parent nodesofa andb until it finds a type annotation on a variable declaration.
In fact, contextual typing only applies to two kinds of things:parameters and literals (including JSX literals). But it may find a type in a variety of places.Here are 3 typical ones:
- A type annotation on a declaration:
typeConfig={before(data:string):void}constcfg:Config={before(x){console.log(x.length)}}
- The left-hand side of an assignment:
letsteps:('up'|'down'|'left'|'right')[]=['up','up','down','down']steps=['down']
- An argument in a function call:
declarefunctionsetup(register:(name:string,age:number)=>void):voidsetup((name,age)=>console.log(name,age))
The basic mechanism of contextual typing is a search for a typeannotation. Once a type annotation is found, contextual typing walksdown through thetype by reversing the path it walked up through thetree.
Aside: In example (2), contextual typing gives'down' thenon-widening type'down'; it would otherwise have the typestring. That means['down'] will have the type'down'[], whichis assignable tosteps. So contextual typing lets programmers avoidwriting['down' as 'down'] in some cases.
Let's walk through example (1).
- During normal check of the tree,
checkFunctionExpressionOrObjectLiteralMethodis called onbefore. - This calls
getApparentTypeofContextualType(after a fewintermediate functions), whichrecursively looks for the contextual type ofbefore's parent. - The parent is an object literal, which recursively looks for thecontextual type of the object literal's parent.
- The parent is a variable declaration with a type annotation
Config.This is the contextual type of the object literal. - Next we look inside
Configfor a property namedbefore. Since'sConfig.before's type is a signature, that signature is thecontextual type ofbefore. - Finally,
assignContextualParameterTypesassigns a type forxfromConfig.before's first parameter.
Note that if you have type annotations on some parameters already,assignContextualParameterTypes will skip those parameters.
Contextually typing(name, age) => ... in (3) works substantiallythat same. When the search reachesgetContextualType, instead of avariable declaration, the parent is a call expression. The contextualtype of a call expression is the type of the callee,setup in thiscase. Now, as before, we look insidesetup's type:(name, age) => ... is the first argument, so its contextual type is from the firstparameter ofsetup,register.assignmentContextualParameterTypesworks forname andage as in (1).
Type parameter inference is quite different from the other twotechniques. It still inferstypes based on providedvalues,but the inferred types don't replace a type annotation. Insteadthey're provided as type arguments to a function, which results ininstantiating a generic function with some specific type. For example:
declarefunctionsetup<T>(config:{initial():T}):Tsetup({initial(){return"last"}})
First checks{ initial() { return "last" } } to get{ initial(): string }. By matchingT in{ initial(): T } withstring in{ initial(): string }, it infers thatT isstring, making thesecond line the same as if the author had written:
setup<string>({initial(){return"last"}})
Meaning that the compiler then checks that{ initial() { return "last" } } is assignable to{ initial(): string }.
Type parameter inference starts off ininferTypeArguments, wherethe first step in type parameter inference is to get the type of allthe arguments to the function whose parameters are being inferred. Inthe above example, the checker says that the type of{ initial() { return "last" } } is{ initial(): string }. Thistype is called thesource type, since it is the source ofinferences. It's matched with the parameter type{ initial(): T }.This is thetarget type -- it contains type parameters which arethe target of the process.
Type parameter inference is a pairwise walk of the two types, lookingfor type parameters in the target, matching them to correspondingtypes in the source. The type is walked structurally sort of like a treeis elsewhere in the compiler.
inferTypesgets called on each source/target pair withargument=source/parameter=target. There's only one pair here:{ initial(): string }and{ initial(): T }.- Since both sides are object types,
inferFromPropertieslooksthrough each property of the target and looks for a match in thesource. In this case both have the propertyinitial. initial's type is a signature on both sides(() => T/() => string), so inference goes toinferFromSignature, whichrecursively infers from the return type.- Now the source/target pair is
T/string. Since the source is alone type parameter, we addstringto the list of candidates forT.
Once all the parameters have hadinferTypes called on them,getInferredTypes condenses each candidate array to a single type,viagetUnionType in this case.T's candidates array is[string],sogetUnionType immediately returnsstring.
Only inference to return types,keyof T and mapped type constraints(which are usuallykeyof too) produce a union. These are allcontravariant inference locations. All other locationscall the custom codegetCommonSupertype, which more or less doeswhat it says. Note that object types are always unioned togetherfirst, regardless of inference position.
Type parameter inference actually operates in two passes. The firstpass skips arguments that have contextually typed expressions so thatif good inferences are found from other arguments, contextual typingcan provide types to parameters of function expressions, which in turnmay produce better return types. Then the second pass proceeds withall arguments.
Different positions have different inference priorities; when the typewalk finds a candidate at a higher priority position than existingcandidates, it throws away the existing candidates and starts overwith the higher-priority candidate. For example, a lone type variablehas the highest priority, but a type variable found inside a return typehas one of the lowest priorities.
Priorities have two important limitations:first, they are defined ad-hoc, based on heuristics developed byobserving bad type inferences and trying to fix them. Second, throwing awaylow-priority inferences is faster, but will miss some inferencescompared to integrating all priorities in some way.
Certain candidates are inferred contravariantly, such as parameters ofcallbacks. This is a separate system from inference priorities;contravariant candidates are even higher priority.
A reverse mapped type is a mapped type that is constructed duringinference, and it requires information obtained from inference, but isnot a central part of inference. A reverse mapped type is constructed whenthe target is a mapped type and the source is an object type. Itallows a inference to apply to every member of an object type:
typeBox<T>={ref:T}typeBoxed<T>={[KinkeyofT]:Box<T[K]>}declarefunctionunbox<T>(boxed:Boxed<T>):T;unbox({a:{ref:1},m:{ref:"1"}})// returns { a: number, m: string }
Reverse mapped types are normal types just like conditional types,index types, mapped types, etc. The difference is that they have noexplicit syntax to construct them.
News
Debugging TypeScript
- Performance
- Performance-Tracing
- Debugging-Language-Service-in-VS-Code
- Getting-logs-from-TS-Server-in-VS-Code
- JavaScript-Language-Service-in-Visual-Studio
- Providing-Visual-Studio-Repro-Steps
Contributing to TypeScript
- Contributing to TypeScript
- TypeScript Design Goals
- Coding Guidelines
- Useful Links for TypeScript Issue Management
- Writing Good Design Proposals
- Compiler Repo Notes
- Deployment
Building Tools for TypeScript
- Architectural Overview
- Using the Compiler API
- Using the Language Service API
- Standalone Server (tsserver)
- TypeScript MSBuild In Depth
- Debugging Language Service in VS Code
- Writing a Language Service Plugin
- Docker Quickstart
FAQs
The Main Repo