Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Reference Checker Inference

Nathan Shively-Sanders edited this pageJul 28, 2023 ·2 revisions

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

Initialiser inference

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:

  1. Look for the type ofx.
  2. There is no annotation, so use the (widened) type of the initialiser:number.
  3. Check that the initialiser's type123 is assignable tonumber.

Contextual typing

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:

  1. A type annotation on a declaration:
typeConfig={before(data:string):void}constcfg:Config={before(x){console.log(x.length)}}
  1. The left-hand side of an assignment:
letsteps:('up'|'down'|'left'|'right')[]=['up','up','down','down']steps=['down']
  1. 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.

Walkthrough

Let's walk through example (1).

  1. During normal check of the tree,checkFunctionExpressionOrObjectLiteralMethod is called onbefore.
  2. This callsgetApparentTypeofContextualType (after a fewintermediate functions), whichrecursively looks for the contextual type ofbefore's parent.
  3. The parent is an object literal, which recursively looks for thecontextual type of the object literal's parent.
  4. The parent is a variable declaration with a type annotationConfig.This is the contextual type of the object literal.
  5. Next we look insideConfig for a property namedbefore. Since'sConfig.before's type is a signature, that signature is thecontextual type ofbefore.
  6. Finally,assignContextualParameterTypes assigns a type forx fromConfig.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

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

Walkthrough

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.

  1. inferTypes gets called on each source/target pair withargument=source/parameter=target. There's only one pair here:{ initial(): string } and{ initial(): T }.
  2. Since both sides are object types,inferFromProperties looksthrough each property of the target and looks for a match in thesource. In this case both have the propertyinitial.
  3. initial's type is a signature on both sides(() => T/() => string), so inference goes toinferFromSignature, whichrecursively infers from the return type.
  4. Now the source/target pair isT/string. Since the source is alone type parameter, we addstring to 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.

Other considerations

Method of Combining Candidate Arrays

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.

Interference Between Contextual Typing and Type Parameter Inference

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.

Inference Priorities

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.

Contravariant Candidates

Certain candidates are inferred contravariantly, such as parameters ofcallbacks. This is a separate system from inference priorities;contravariant candidates are even higher priority.

Reverse Mapped Types

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.

Want to contribute to this Wiki?

Fork it and send a pull request.

News

Debugging TypeScript

Contributing to TypeScript

Building Tools for TypeScript

FAQs

The Main Repo

Clone this wiki locally


[8]ページ先頭

©2009-2025 Movatter.jp