Preface
The official documentation of TypeScript has long been updated, but the Chinese documents I can find are still in the older version. Therefore, some chapters that have been newly added and revised have been personally translated and sorted out.
This article is compiled from https://www.typescriptlang.org/docs/handbook/2/narrowing.html
This article does not completely follow the translation of the original text. It also explains and supplements some of the content.
Narrowing
Imagine we have such a function, the function is called padLeft:
function padLeft(padding: number | string, input: string): string {
throw new Error("Not implemented yet!");
}
The functions implemented by this function are:
If the parameter padding
is a number, we add the same number of spaces input
padding
is a string, we directly add it in front of input
Let's implement this logic:
function padLeft(padding: number | string, input: string) {
return new Array(padding + 1).join(" ") + input;
// Operator '+' cannot be applied to types 'string | number' and 'number'.
}
If you write in this way, padding + 1
the editor will be marked in red, indicating an error.
This is TypeScript warning us that if we add a number
type (that is, the number 1 in the example) and a number | string
type, it may not achieve the result we want. In other words, we should first check padding
is a number
, or deal with padding
is string
, then we can do this:
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return new Array(padding + 1).join(" ") + input;
}
return padding + input;
}
This code may not seem interesting, but in fact, TypeScript does a lot of things behind it.
TypeScript must learn to analyze the specific types of statically typed values at runtime. At present, TypeScript has implemented if/else
, ternary operators, loops, and truth checking.
In our if
statement, TypeScript will think that typeof padding === number
is a special form of code, we call it type guard (type guard) , TypeScript will follow the possible path during execution, and the analysis value will be in a given position The most specific type.
TypeScript's type checker will take these type protection and assignment statements into consideration, and this the type to a more precise type is called narrowing . In the editor, we can observe the type change:
From the figure above, we can see that in the if
statement and the remaining return
statements, padding
is deduced to a more precise type.
Next, we will introduce the various contents involved in narrowing
typeof type guards
JavaScript itself provides the typeof
operator, which can return the basic type information of a value at runtime, and will return the following specific strings:
- "string"
- "number"
- "bigInt"
- "boolean"
- "symbol"
- "undefined"
- "object"
- "function"
typeof
operator is widely used in many JavaScript libraries, and TypeScript can already understand and narrow the types in different branches.
In TypeScript, checking typeof
is a type of protection. TypeScript knows the typeof
different values of 061d45ce92cbb4, and it can also recognize some weird places in JavaScript. For example, in the above list, typeof
does not return the string null
. Look at the following example:
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
for (const s of strs) {
// Object is possibly 'null'.
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// do nothing
}
}
In this printAll
function, we try to determine whether strs
is an object. The original purpose is to determine whether it is an array type, but in JavaScript, typeof null
will also return object
. And this is an unfortunate historical accident of JavaScript.
Skilled users will naturally not be surprised, but not everyone is so skilled. Fortunately, TypeScript will let us know that strs
is narrowed to strings[] | null
, not just string[]
.
Truthiness narrowing
In JavaScript, we can use any expression in conditional statements, such as &&
, ||
, !
etc. For example, a if
does not require a condition and the result is always of type boolean
function getUsersOnlineMessage(numUsersOnline: number) {
if (numUsersOnline) {
return `There are ${numUsersOnline} online now!`;
}
return "Nobody's here. :(";
}
This is because JavaScript will do implicit type conversions, such as 0
, NaN
, ""
, 0n
, null
undefined
these values will be converted to false
true
will be converted to 061d45ce92cc7d.
Of course, you can also use the Boolean
function to force the boolean
, or use the shorter !!
:
// both of these result in 'true'
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true, value: true
This method of use is very popular, especially suitable for preventing values like null
and undefiend
For example, we can use this printAll
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
It can be seen that in this way, the error was successfully removed.
But it should be noted that truth checking on basic types can easily lead to errors. For example, if we write the printAll
function like this:
function printAll(strs: string | string[] | null) {
// !!!!!!!!!!!!!!!!
// DON'T DO THIS!
// KEEP READING
// !!!!!!!!!!!!!!!!
if (strs) {
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
}
We wrap the contents of the original function body in a if (strs)
truth value check. There is a problem here, that is, we cannot handle the empty string correctly. If the passed in is an empty string, the truth check judges to be false
, and it will enter the wrong processing branch.
If you are not familiar with JavaScript, you should be aware of this situation.
Another way to narrow down the type through truth checking is through the !
operator.
function multiplyAll(
values: number[] | undefined,
factor: number
): number[] | undefined {
if (!values) {
return values;
// (parameter) values: undefined
} else {
return values.map((x) => x * factor);
// (parameter) values: number[]
}
}
Equality narrowing
Typescript will also use switch
statements and equivalence checks such as ==
!==
==
!=
to narrow down the type. for example:
In this example, we judged whether x
and y
are completely equal. If they are completely equal, then their types must be completely equal. The string
type is the only possible same type of x
and y
So in the first branch, x
and y
must be of type string
Judging the specific literal value also allows TypeScript to correctly determine the type. In the previous section of truth-value narrowing, we wrote a printAll
that did not handle the empty string correctly. Now we can use a more specific judgment to rule out the null
:
JavaScript loose equality operators such as ==
and !=
can also be narrowed correctly. In JavaScript, the == null
method does not accurately determine that this value is null
, it may also be undefined
. The == undefined
, but with this point, we can easily determine whether a value is neither null
nor undefined
:
in operator narrows
There is a in
operator in JavaScript that can determine whether an object has a corresponding attribute name. TypeScript can also use this to narrow down types.
For example, in "value" in x
, "value"
is a string literal, and x
is a union type:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
// (parameter) animal: Fish
}
return animal.fly();
// (parameter) animal: Bird
}
Through "swim" in animal
, we can accurately narrow down the type.
And if there are optional attributes, such as a human being either swim
or fly
(with equipment), it can also be displayed correctly:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };
function move(animal: Fish | Bird | Human) {
if ("swim" in animal) {
animal; // (parameter) animal: Fish | Human
} else {
animal; // (parameter) animal: Bird | Human
}
}
instanceof narrows
instanceof
is also a type protection, TypeScript can also narrow down by identifying the correct type of instanceof
Assignments
TypeScript can correctly narrow the left value according to the right value of the assignment statement.
Note that these assignments are valid, even if we have x
changed number
type, but we can still change it to string
type, because x
initially declared string | number
, when the assignment will be based on a formal declaration Check.
So if we assign x
to a boolean type, an error will be reported:
Control flow analysis
So far we have talked about some basic examples of narrowing types in TypeScript. Now we look at the type protection in conditional control statements such as if
while
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return new Array(padding + 1).join(" ") + input;
}
return padding + input;
}
In the first if
statement, because there is a return
statement, TypeScript can determine the remaining part return padding + input
through code analysis. If the padding is of number
, it cannot be reached ( unreachable is in the remaining part) , The number
type will be deleted from the number | string
type.
This code analysis based on reachability ( reachability ) is called control flow analysis. When encountering type protection and assignment statements, TypeScript uses this method to narrow down the type. In this way, a variable can be observed to become a different type:
Type predicates
In some documents, type predicates
will be translated into type predicate . Considering that predicate as a verb also has the meaning of declaration, declaration, and assertion, which is distinguished from Type Assertion. Here I simply translate it into a type predicate.
If you quote this explanation:
In mathematics, a predicate is commonly understood to be a Boolean-valued function_ P_: _X_→ {true, false}, called the predicate on _X_.
The so-called predicate
is a function that returns the value of boolean
Then we continue to look down.
If you want to control the type change directly through the code, you can customize a type protection. The implementation method is to define a function, and the type returned by this function is a type judgment. An example is as follows:
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
In this case, pet is Fish
is our type of predicate, a predicate type using parameterName is Type
form, but parameterName
must be current parameter name of the function.
When isFish is called by passing in a variable, TypeScript can narrow this variable to a more specific type:
// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();
if (isFish(pet)) {
pet.swim(); // let pet: Fish
} else {
pet.fly(); // let pet: Bird
}
Note that, TypeScript does not just know if
statement in the pet
is Fish
type, also know else
branch where, pet
is Bird
type, after all pet
on two possible types.
You can also use isFish
in Fish | Bird
to filter to obtain an array of only type Fish
const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()];
const underWater1: Fish[] = zoo.filter(isFish);
// or, equivalently
const underWater2: Fish[] = zoo.filter(isFish) as Fish[];
// 在更复杂的例子中,判断式可能需要重复写
const underWater3: Fish[] = zoo.filter((pet): pet is Fish => {
if (pet.name === "sharkey") return false;
return isFish(pet);
});
Discriminated unions (Discriminated unions)
Let us imagine such a process Shape
(such Circles
, Squares
function), Circles
records its radius properties Squares
records its side lengths properties, we use a kind
field to distinguish the determination process is Circles
or Squares
, which Is the initial definition Shape
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}
Note that here we use a union type, "circle" | "square"
, instead of a string
, we can avoid some spelling errors:
function handleShape(shape: Shape) {
// oops!
if (shape.kind === "rect") {
// This condition will always return 'false' since the types '"circle" | "square"' and '"rect"' have no overlap.
// ...
}
}
Now we write a getArea
function to get the area, and the way to calculate the area of a circle and a square is different, let's deal with the case Circle
function getArea(shape: Shape) {
return Math.PI * shape.radius ** 2; // 圆的面积公式 S=πr²
// Object is possibly 'undefined'.
}
In strictNullChecks
mode, TypeScript will complain, after all radius
value may indeed be undefined
, that if we According kind
judgment about it?
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
// Object is possibly 'undefined'.
}
}
You will find that TypeScript is still kind
errors, even if we judge that 061d45ce92d2ab is circle
, but because radius
is an optional attribute, TypeScript still thinks that radius
may be undefined
.
We can try to assert a non-empty (non-null assertion), that shape.radius
plus a !
to represent radius
must exist.
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius! ** 2;
}
}
But this is not a good way. We have to use a non-empty assertion to convince the type checker that shape.raidus
exists at this time. We set it as an optional attribute when defining the radius, but here we consider it to be certain. Existence, the semantics before and after are also inconsistent. So let us think about how we can better define it.
Shape
at this time is that the type checker has no way to determine whether the radius
and sideLength
kind
attribute. This is what we need to tell the type checker, so we can define Shape
like this:
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
Here, we Shape
according kind
into two different types of properties, radius
and sideLength
are as defined in the respective types required
.
Let us see what happens if we get radius
directly?
function getArea(shape: Shape) {
return Math.PI * shape.radius ** 2;
Property 'radius' does not exist on type 'Shape'.
Property 'radius' does not exist on type 'Square'.
}
As we defined Shape
first time, there are still errors.
When defining radius
as optional
at the beginning, we will get an error (in strickNullChecks
mode), because TypeScript cannot determine that this attribute must exist.
And now the error is reported because Shape
is a joint type, TypeScript can recognize that shape
may also be a Square
, and Square
does not have radius
, so an error will be reported.
But at this time, should we kind
attribute?
You will find that the error is removed in this way.
When the union type in each type, contains a common literal types of properties, TypeScript will think this is a discernible joint (discriminated of Union) , then a member of a specific type can be narrowed.
In this example, kind
is the public attribute (discriminant as Shape).
This also applies to the switch
statement:
The key here is how to define Shape
and tell TypeScript that Circle
and Square
are two types that are completely separated based on the kind
In this way, the type system can deduce the correct type in each branch of the switch
Distinguishable joint applications are far more than these, such as message mode, such as client-server interaction, and for example in the state management framework, are very practical.
Imagine that in the message mode, we will monitor and send different events. These are distinguished by names, and different events will carry different data. This is applied to distinguishable unions. The interaction and state management between the client and the server are similar.
never type
When narrowing down, if you exhaust all possible types, TypeScript will use a never
type to represent an impossible state.
Let us continue to look down.
Exhaustiveness checking
The never type can be assigned to any type, however, no type can be assigned to never
(except never
itself). This means that you can never
switch
statement to do an exhaustive check.
For example, add a default
getArea
function, assign shape
to the never
type, when there is a branch that has not been processed, never
will come into play.
type Shape = Circle | Square;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
When we Shape
type, but do not do the corresponding processing, it will cause a TypeScript error:
interface Triangle {
kind: "triangle";
sideLength: number;
}
type Shape = Circle | Square | Triangle;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
// Type 'Triangle' is not assignable to type 'never'.
return _exhaustiveCheck;
}
}
Because of the narrowing characteristics of TypeScript, when the execution reaches default
, the type is narrowed to Triangle
, but because any type cannot be assigned to the never
type, this will cause a compilation error. In this way, you can ensure that the getArea
function always shape
all the possibilities of 061d45ce92d58f.
TypeScript series
The TypeScript series of articles consists of three parts: official document translation, important and difficult analysis, and practical skills. It covers entry, advanced, and actual combat. It aims to provide you with a systematic learning TS tutorial. The entire series is expected to be about 40 articles. Click here to browse the full series of articles, and suggest to bookmark the site by the way.
WeChat: "mqyqingfeng", add me to the only reader group in Kongyu.
If there are mistakes or not rigorous, please correct me, thank you very much. If you like or have some inspiration, star is welcome, which is also an encouragement to the author.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。