[TypeScript学习笔记-04]频繁使用的各种Type Narrowing手段

TypeScript 的类型系统是一种静态契约机制,它定义了值在编译期必须满足的结构和行为约束。类型可以描述对象的成员(名称、类型、可选性、可写性)、函数签名、字面量取值范围等。联合类型 A | B 表示一个值可能是 A 或 B,从集合论角度看是类型集合的并集,但它不会把成员结构合并,而是保持"二选一"的语义。所以当我们将一个变量声明为联合类型的时候,在默认的情况下只有针对交集 数据成员的访问是安全的。为了不仅限于访问联合类型的交集成员,但是并集成员范围又太宽,在编程的时候我们需要引入响应的条件判断,缩小成员的范围,使针对某些API的访问控制在一个安全的边界之内,我们将其称为类型收窄(Narrowing)。

1. typeof操作符

JavaScript 支持 typeof 运算符,它可以提供运行时值类型的基本信息。TypeScript 期望它返回一组特定的字符串

  • "string"
  • "number"
  • "bigint"
  • "boolean"
  • "symbol"
  • "undefined"
  • "object"
  • "function"

typeof null === "object" 是成立的,所以下面代码中的strs可能为null,具体类型为string[]|null

typescript 复制代码
function printAll(strs: string | string[] | null) {
  if (typeof strs === "object") {
    for (const s of strs) {
       //'strs' is possibly 'null'. strs的类型为 string[]
      console.log(s);
    }
  } else if (typeof strs === "string") {
    console.log(strs);
  } 
}

2. 基于真值类型的条件判断

在 JavaScript 中,我们可以在条件语句、&&||if 语句、布尔否定(!)等中使用任何表达式 ,并不要求其条件必须是布尔类型。在 JavaScript 中,类似 if 这样的语句首先会将条件强制为布尔值以使其有意义,然后根据结果是真还是假来选择执行分支。

  • 0
  • NaN
  • "" (the empty string)
  • 0n (the bigint version of zero)
  • null
  • undefined

所以上面的问题可以通过 && 操作符来确保strs不可能为null

typescript 复制代码
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);
  }
}

3. 对等判断

TypeScript 还使用 switch 语句和相等性检查(例如 ===!==== !=)来缩小类型范围。

typescript 复制代码
function example(x: string | number, y: string | boolean) {
  if (x === y) { // 既然对等,必然是字符串
    x.toUpperCase();          
    y.toLowerCase();          
  } else {
    console.log(x); //(parameter) x: string | number
    console.log(y); //(parameter) y: string | boolean
  }
}

: == /!====/!==的区别:

typescript 复制代码
interface Container {
  value: number | null | undefined;
}
 
function multiplyValue(container: Container, factor: number) {
  // 排除null和undefined.
  if (container.value != null) {
    console.log(container.value);                           
    // (property) Container.value: number 
    container.value *= factor;
  }
}

4. in 操作符

运算符 in用于判断一个对象或其原​​型链 是否具有名为 name 的属性。TypeScript 会利用这一点来缩小潜在类型的范围:

typescript 复制代码
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
  }
}

:对于可缺省数据成员,可能存在,也可能不存在。

5. instanceof 操作符

运算符 instanceof 可以检查一个值是否是另一个值的实例 。更具体地说,在 JavaScript 中,x instanceof Foo 检查 x原型链 是否包含 Foo.prototype

typescript 复制代码
function logValue(x: Date | string) {
  if (x instanceof Date) {
    console.log(x.toUTCString()); // (parameter) x: Date
  } else {
    console.log(x.toUpperCase()); // (parameter) x: string
  }
}

6. 赋值语句

当我们给任何变量赋值时,TypeScript 会查看赋值语句的右侧,并相应地缩小左侧的范围。如下的代码是合法的:

typescript 复制代码
let x = Math.random() < 0.5 ? 10 : "hello world!"; //let x: string | number

x = 1; 
console.log(x); //let x: number

x = "goodbye!"; 
console.log(x);  //let x: string

如下则是有问题的:

typescript 复制代码
let x = Math.random() < 0.5 ? 10 : "hello world!"; //let x: string | number

x = 1;
console.log(x);  //let x: number

x = true; // 类型不匹配,赋值失败.
 
console.log(x); // let x: string | number

7. 控制流分析

TypeScript会自动分析控制流,并在相应分支智能实施类型收窄。

typescript 复制代码
function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    return " ".repeat(padding) + input;
  }
  return padding + input; // 排除number,只能是string
}

8. 类型断言

要定义用户自定义type guards,我们只需要定义一个返回类型为类型谓词的函数:

typescript 复制代码
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

let pet = getSmallPet(); 
if (isFish(pet)) {
  pet.swim();
} else {
  pet.fly();
}

:TypeScript函数的两种返回形式:基于确定类型的值 + 类型谓词

9. assert 函数

assert 函数可以在调用之后根据指定的断言强行实施类型收窄。

typescript 复制代码
function multiply(x, y) {
  assert(typeof x === "number");
  assert(typeof y === "number");
  return x * y;
}

10. 可区分的联合(Discriminated Unions)

在 TypeScript 中,可区分联合类型(Discriminated Unions)是一种通过共享的判别字段 来安全区分不同对象类型的模式。它能让编译器在 switchif 判断时自动缩小类型范围,并提供 穷尽性检查,避免遗漏分支。

它用来解决这种问题:

typescript 复制代码
interface Shape {
  kind: "circle" | "square";
  radius?: number;
  sideLength?: number;
}

function getArea(shape: Shape) {
  return shape.kind == "circle" 
    ? Math.PI * shape.radius ** 2 //'shape.radius' 可能是'undefined'.
    : shape.sideLength ** 2 ;     //'shape.sideLength' 可能是'undefined'.
}

解决方案:

typescript 复制代码
interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape): number {
  return shape.kind == "circle" 
    ? Math.PI * shape.radius ** 2 
    : shape.sideLength ** 2 ; 
}

11. 穷尽性检查(Exhaustiveness checking)

缩小范围时,你可以将联合类型的选项减少到排除所有可能性的程度 。在这种情况下,TypeScript 会使用 never 类型来表示不应该存在的状态。never 类型可以赋值给任何类型;然而,除了 never 本身之外,没有任何类型可以赋值给 never。这意味着你可以使用类型缩小,并依靠 never 的出现来进行 switch 语句中的穷举检查。

typescript 复制代码
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;
  }
}
相关推荐
日光倾1 天前
TypeScript 随手记 —— 1
前端·javascript·typescript
werdedeage1 天前
从一个 loading 到完整状态机:多模型图片编辑流程的前端建模
typescript
JaydenAI1 天前
[TypeScript学习笔记-10]一个特殊且重要的类型:Symbol
typescript·symbol
To_OC2 天前
啃完 TS 工具类型我发现:Pick 和 Omit 原来就是一层窗户纸
前端·面试·typescript
JaydenAI2 天前
[TypeScript学习笔记-07]根据现有类型创建新类型的N种方式
typescript
东风破_2 天前
TypeScript 高级类型进阶:keyof、Exclude、Record 与类型组合思想
前端·后端·typescript
晓说前端2 天前
TypeScript 核心语法应用 —— Vue 3 中的使用(下)
前端·javascript·typescript·类型系统
东风破_2 天前
(拼多多考题)TypeScript 工具类型:Pick、Omit、Partial、Required
typescript
rimydu1974art2 天前
opencheck解读:拆解 OpenCheck 的歧视性条款识别与澄清问询机制
人工智能·typescript
触底反弹3 天前
🔥 从「为什么」到「怎么用」:TypeScript 类型约束与泛型完全指南
后端·面试·typescript