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 Widening Narrowing

Nathan Shively-Sanders edited this pageSep 2, 2022 ·1 revision

Widening and Narrowing in Typescript

Typescript has a number of related concepts in which a type getstreated temporarily as a similar type. Most of these concepts areinternal-only. None of them are documented very well. For the internalconcepts, we expect nobody needs to know about them to use thelanguage. For the external concepts, we hope that they work wellenough that most peoplestill don't need to think about them. Thisdocument explains them all, aiming to help two audiences: (1) advancedusers of Typescript whodo need to understand the quirks of thelanguage (2) contributors to the Typescript compiler.

The concepts covered in this document are as follows:

  1. Widening: treat an internal type as a normal one.
  2. Literal widening: treat a literal type as a primitive one.
  3. Narrowing: remove constituents from a union type.
  4. Instanceof narrowing: treat a type as a subclass.
  5. Apparent type: treat a non-object type as an object type.

Widening

Widening is the simplest operation of the bunch. The typesnull andundefined are converted toany. This happensrecursively in object types, union types, and array types (includingtuples).

Why widening? Well, historically,null andundefined were internaltypes that needed to be converted toany for downstream consumersand for display. With--strictNullChecks, widening doesn't happenany more. But without it, widening happens a lot, generally when obtaininga type from another object. Here are some examples:

//@strict: falseletx=null;

Here,null has the typenull, butx has the typeany becauseof widening on assignment.undefined works the same way. However,with--strict,null is preserved, so no widening will happen.

Literal widening

Literal widening is significantly more complex than "classic"widening. Basically, when literal widening happens, a literal typelike"foo" orSomeEnum.Member gets treated as its base type:string orSomeEnum, respectively. The places where literals widen,however, cause the behaviour to be hard to understand. Literalwidening is described fullyat the literal widening PRandits followup.

When does literal widening happen?

There are two key points to understand about literal widening.

  1. Literal widening only happens to literal types that originate fromexpressions. These are calledfresh literal types.
  2. Literal widening happens whenever a fresh literal type reaches a"mutable" location.

For example,

constone=1;// 'one' has type: 1letnum=1;// 'num' has type: number

Let's break the first line down:

  1. 1 has the fresh literal type1.
  2. 1 is assigned toconst one, soone: 1. But the type1 is stillfresh! Remember that for later.

Meanwhile, on the second line:

  1. 1 has the fresh literal type1.
  2. 1 is assigned tolet num, a mutable location, sonum: number.

Here's where it gets confusing. Look at this:

constone=1;letwat=one;// 'wat' has type: number

The first two steps are the same as the first example. The third step

  1. 1 has the fresh literal type1.
  2. 1 is assigned toconst one, soone: 1.
  3. one is assigned towat, a mutable location, sowat: number.

This is pretty confusing! The fresh literal type1 makes its waythrough the assignment toone down to the assignment towat. Butif you think about it, this is what you want in a real program:

conststart=1001;constmax=100000;// many (thousands?) of lines later ...for(leti=start;i<max;i=i+1){// did I just write a for loop?// is this a C program?}

If the type ofi were1001 then you couldn't write a for loop basedon constants.

There are other places that widen besides assignment. Basically it'sanywhere that mutation could happen:

constnums=[1,2,3];// 'nums' has type: number[]nums[0]=101;// because Javascript arrays are always mutableconstdoom={e:1,m:1}doom.e=2// Mutable objects! We're doomed!// Dooomed!// Doomed!// -gasp- Dooooooooooooooooooooooooooooooooo-

What literal types widen?

  • Number literal types like1 widen tonumber.
  • String literal types like'hi' widen tostring.
  • Boolean literal types liketrue widen toboolean.
  • Enum members widen to their containing enum.

An example of the last is:

enumState{Start,Expression,Term,End}conststart=State.Start;letstate=start;letch='';while(ch=nextChar()){switch(state){// ... imagine your favourite tokeniser here}}

Narrowing

Narrowing is essentially the removal of types from a union. It'shappening all the time as you write code, especially if you use--strictNullChecks. To understand narrowing, you first need tounderstand the difference between "declared type" and "computed type".

The declared type of a variable is the one it's declared with. Forlet x: number | undefined, that'snumber | undefined. The computedtype of a variable is the type of the variable as it's used incontext. Here's an example:

//@strict: truetypeThing={name:'one'|'two'};functionprocess(origin:Thing,extra?:Thing|undefined):void{preprocess(origin,extra);if(extra){console.log(extra.name);if(extra.name==='one'){// ...

extra's declared type isThing | undefined, since it's an optionalparameter. However, its computed type varies based on context. On thefirst line, inpreprocess(origin, extra), its computed type is stillThing | undefined. However, inside theif (extra) block,extra'scomputed type is now justThing because it can't possibly beundefined due to theif (extra) check. Narrowing has removedundefined from its type.

Similarly, the declared type ofextra.name is'one' | 'two', butinside the true branch ofif (extra.name === 'one'), its computedtype is just'one'.

Narrowing mostly commonly removes all but one type from a union, butdoesn't necessarily need to:

typeType=Anonymous|Class|Interfacefunctionf(thing:string|number|boolean|object){if(typeofthing==='string'||typeofthing==='number'){returnlookup[thing];}elseif(typeofthing==='boolean'&&thing){returnglobalCachedThing;}else{returnthing;}}

Here, in the first if-block,thing narrows tostring | number becausethe check allows it to be either string or number.

Instanceof Narrowing

Instanceof narrowing looks similar to normal narrowing, andbehaves similarly, but its rules are somewhat different. It onlyapplies to certaininstanceof checks and type predicates.

Here's a use ofinstanceof that follows the normal narrowing rules:

classC{c:any}functionf(x:C|string){if(xinstanceofC){// x is C here}else{// x is string here}}

So far this follows the normal narrowing rules. Butinstanceofapplies to subclasses too:

classDextendsC{d:any}functionf(x:C){if(xinstanceofD){// x is D here}else{// x is still just C here}}

Unlike narrowing,instanceof narrowing doesn't remove any types togetx's computed type. It just notices thatD is a subclass ofCand changes the computed type toD inside theif (x instanceof D)block. In theelse blockx is stillC.

If you mess up the class relationship, the compiler does its bestto make sense of things:

classE{e:any}// doesn't extend C!functionf(x:C){if(xinstanceofE){// x is C & E here}else{// x is still just C here}}

The compiler thinks that something of typeC can't also beinstanceof E, but just in case, it sets the computed type ofx toC & E, so that you can use the properties ofE in the block— just be aware that the block will probably never execute!

Type predicates

Type predicates follow the same rules asinstanceof when narrowing,and are just as subject to misuse. So this example is equivalent tothe previous wonky one:

functionisE(e:any):e isE{returne.e;}functionf(x:C){if(isE(x)){// x is C & E here}else{// nope, still just C}}

Apparent Type

In some situations you need to get the properties on a variable, evenwhen it technically doesn't have properties. One example is primitives:

letn=12lets=n.toFixed()

12 doesn't technically have properties;Number does. In order tomapnumber toNumber, we defineNumber as theapparent type ofnumber. Whenever the compiler needs to get properties of some type,it asks for the apparent type of that type first. This applies toother non-object types like type parameters:

interfaceNode{parent:Node;pos:number;kind:number;}functionsetParent<TextendsNode>(node:T,parent:Node):T{node.parent=parent;returnnode;}

T is a type parameter, which is just a placeholder. But itsconstraint isNode, so when the compiler checksnode.parent, itgets the apparent type ofT, which isNode. Then it sees thatNode has aparent property.

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