Type guards and type narrowing
Type Guards and Type Narrowing Concepts
TypeScript's type system allows developers to more precisely control the type scope of variables through type guards and type narrowing. A type guard is a runtime check used to determine the specific type of a variable, while type narrowing is the TypeScript compiler automatically reducing the type scope of a variable to a more specific type based on these checks. These two concepts are closely related and work together to improve type safety in code.
typeof
Type Guards
typeof
is one of the most basic type guards. It narrows the TypeScript type scope by checking the JavaScript type of a variable. When the typeof
operator is used on a variable, TypeScript can recognize specific type patterns and adjust type inference accordingly.
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
In this example, typeof padding === "number"
lets TypeScript know that within the first conditional block, padding
must be of type number
, allowing safe numerical operations.
instanceof
Type Guards
For custom classes or built-in objects, the instanceof
operator is a more suitable type guard. It checks whether the object's prototype chain contains the prototype
property of a specific constructor.
class Bird {
fly() {
console.log("flying");
}
}
class Fish {
swim() {
console.log("swimming");
}
}
function move(pet: Bird | Fish) {
if (pet instanceof Bird) {
pet.fly(); // Here, `pet` is narrowed to type `Bird`
} else {
pet.swim(); // Here, `pet` is narrowed to type `Fish`
}
}
Custom Type Guards
When built-in type guards are insufficient, you can define your own type predicate functions. A custom type guard is a function that returns a type predicate in the form parameterName is Type
.
interface Cat {
meow(): void;
}
interface Dog {
bark(): void;
}
function isCat(animal: Cat | Dog): animal is Cat {
return (animal as Cat).meow !== undefined;
}
function animalSound(animal: Cat | Dog) {
if (isCat(animal)) {
animal.meow(); // Type narrowed to `Cat`
} else {
animal.bark(); // Type narrowed to `Dog`
}
}
Discriminated Unions
Discriminated unions are a powerful tool for type narrowing. They require each member of the union type to have a common singleton type property (i.e., a "tag").
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; sideLength: number }
| { kind: "triangle"; base: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2; // `shape` narrowed to circle
case "square":
return shape.sideLength ** 2; // `shape` narrowed to square
case "triangle":
return (shape.base * shape.height) / 2; // `shape` narrowed to triangle
}
}
Truthiness Narrowing
In JavaScript, conditional checks automatically convert values to booleans. TypeScript leverages this for type narrowing, especially when dealing with values that might be null
or undefined
.
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") {
for (const s of strs) { // `strs` narrowed to `string[]`
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs); // `strs` narrowed to `string`
}
}
Equality Narrowing
TypeScript uses equality checks like ===
, !==
, ==
, and !=
to narrow types. This is particularly useful when working with literal types.
function example(x: string | number, y: string | boolean) {
if (x === y) {
// Here, both `x` and `y` are narrowed to `string`
x.toUpperCase();
y.toLowerCase();
} else {
console.log(x); // `x`: string | number
console.log(y); // `y`: string | boolean
}
}
in
Operator Narrowing
The in
operator checks whether an object has a specific property, and TypeScript uses this for type narrowing.
type Admin = {
name: string;
privileges: string[];
};
type Employee = {
name: string;
startDate: Date;
};
function printInformation(emp: Admin | Employee) {
console.log("Name: " + emp.name);
if ("privileges" in emp) {
console.log("Privileges: " + emp.privileges); // `emp` narrowed to `Admin`
}
if ("startDate" in emp) {
console.log("Start Date: " + emp.startDate); // `emp` narrowed to `Employee`
}
}
Type Assertions and Narrowing
In some cases, developers know more about a value's type than TypeScript does. Type assertions can be used to force type narrowing, but note that they do not perform runtime checks.
function getLength(input: string | number): number {
if ((input as string).length) {
return (input as string).length; // Assert `input` as `string`
} else {
return input.toString().length; // `input` as `number`
}
}
Control Flow Analysis
TypeScript's control flow analysis tracks type changes of variables in code, enabling automatic type narrowing. This analysis is continuous and changes with the execution path.
function example() {
let x: string | number | boolean;
x = Math.random() < 0.5;
console.log(x); // `x`: boolean
if (Math.random() < 0.5) {
x = "hello";
console.log(x); // `x`: string
} else {
x = 100;
console.log(x); // `x`: number
}
return x; // `x`: string | number
}
never
Type and Exhaustiveness Checking
The never
type represents values that should never occur. It is often used for exhaustiveness checking to ensure all possible types are handled.
type Shape = Circle | Square;
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
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;
}
}
If a new Shape
type is added later but getArea
is not updated, TypeScript will report an error in the default
branch because shape
cannot be assigned to the never
type.
Type Narrowing with Generics
Type narrowing also applies in generic functions, but special attention is needed for type parameter constraints.
function process<T extends string | number>(value: T): T {
if (typeof value === "string") {
// Here, `value` is narrowed to `T & string`
return value.toUpperCase() as T;
} else {
// Here, `value` is narrowed to `T & number`
return (value * 2) as T;
}
}
Limitations of Type Narrowing
Although type narrowing is powerful, TypeScript may fail to correctly infer types in certain scenarios, especially with complex logic or asynchronous code.
function isStringArray(arr: any[]): arr is string[] {
return arr.every(item => typeof item === "string");
}
async function fetchData() {
const response = await fetch("/api/data");
const data = await response.json();
if (isStringArray(data)) {
// Here, `data` should be `string[]`, but TypeScript may not fully confirm it
return data.map(s => s.toUpperCase());
}
return [];
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:类型推断与类型断言
下一篇:Vue.js核心知识点