TypeScript 类型范围缩小

我们来写个 padLeft 函数:

function padLeft(padding: number | string, input: string): string {
    // ...
}


他的功能为:如果 padding 是 number 类型,那么在 input 前增加 padding 个空格,如果 padding 是 string 类型,那么直接把 padding 加到 input 前面。我们先实现下 number 类型的逻辑:

function padLeft(padding: number | string, input: string) {


  return " ".repeat(padding) + input;
}


repeat 调用处报错了,因为 repeat参数要求是 number 类型,但现在 padding 是 number | string 类型。所以应该单独处理 string 类型和 number 类型的情况。

function padLeft(padding: number | string, input: string) {


  if (typeof padding === "number") {

    return " ".repeat(padding) + input;

  }



  return padding + input;

}



这种处理看起来就像是无聊的 JavaScript 代码,但这就是重点,因为 TypeScript 即使添加了类型,但也没有改变 JavaScript 的代码编写方式。

也许这段代码看起来 TypeScript 也没做太多事,但事实上它做了很多,它将类型分析覆盖在 JavaScript 的运行时控制流构造上,如  if/else 、三元控制、循环、真实性检查等,这些都可能影响类型。

在上段代码的 if 检查中,TypeScript 看到  typeof padding === "number"  并将其理解为一种称为类型保护的特殊形式的代码。它会根据静态代码来推断类型,这种方式叫做类型范围缩小(Narrowing),如 if 语句块内,padding 的类型就是 number,这甚至可以在编辑器中看到:

image.png

TypeScript 有几种不同的方式能做到类型范围缩小。

typeof 类型检查

JavaScript 支持  typeof  运算符,它可以提供运行时值类型的基本信息,并返回一个字符串结果:

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

TypeScript 可以通过如上结果识别类型,并且它还知道 JavaScript 的特殊情况(typeof null 的值是 object)

image.png

真值类型检查

真值是在 JavaScript 中经常听到的东西,可以在条件表达式、 && s、 || s、 if  语句、布尔否定 ( ! ) 等中使用。

function getUsersOnlineMessage(numUsersOnline: number) {
  if (numUsersOnline) {
    return `There are ${numUsersOnline} online now!`;
  }



  return "Nobody's here. :(";
}



numUsersOnline 的类型不是 boolean,这里也不会报错,因为 JavaScript 会把值强制转换成 boolean,下面的值在布尔转换时为 false:

  • 00n (bigint 类型的 0)
  • NaN
  • "" (空字符串)
  • null
  • undefined

你也可以通过 Boolean 函数或 !! 强制转换成布尔。如下代码:

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);
  }
}

if (strs && 这里去除掉了 strs 为 null 的情况,但你这样使用时也要注意:

function printAll(strs: string | string[] | null) {

  if (strs) {
    if (typeof strs === "object") {
      for (const s of strs) {
        console.log(s);
      }
    } else if (typeof strs === "string") {
      console.log(strs);
    }
  }
}

如果 if (strs) 被放到了顶层,那么会过滤掉 null 和 空字符串的情况,通常过滤掉空字符串不是期望的,所以要谨慎。

相等类型检查

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

image.png

x 的类型是 string | number,y 的类型是 string | boolean,如果 x 等于 y,那么只有一种可能,那就是他们都是 string 类型,所以 if 语句块内可以使用 string 的方法。

直接对于特殊值判断相等或不等也能使类型范围缩小

image.png

strs !== null 就排除了 null 类型,typeof strs === "object"也能判断出是数组。

对于双等于 null 这种也能正确判断出类型(在 JavaScript 中,双等于 null 代表这个值可能是 null 或 undefined,当然双等于 undefined 也是同样的)。

image.png

in 运算符

JavaScript 使用  in  运算符来确定对象或其原型链上是否具有某个属性。 TypeScript 将这一点作为类型范围缩小的一种方式。

例如,使用代码: "value" in x 。其中  "value"  是字符串文字, x  是联合类型。 “true”分支代表  x  具有  value  属性,“false”分支反之。

image.png

再看个例子:

image.png

instanceof

JavaScript 使用 instanceof 操作符检查是否为类的实例,更详细说:x instanceof Foo 会检查 x 的原型链上是否有 Foo.prototype。所以这也是类型范围缩小的一种方式

image.png

赋值

在赋值时,TypeScript 推断右边类型,并设置给左边

image.png

后面代码赋值给 number 或是 string 都是合法的,但如果赋值 boolean 就会报错

image.png

控制流分析

function padLeft(padding: number | string, input: string) {


  if (typeof padding === "number") {

    return " ".repeat(padding) + input;

  }



  return padding + input;

}



if 块里面 padding 是 number 类型,外面 padding 是 string 类型,padLeft 返回值是 string 类型。

不同的控制流可以使类型不断改变:

image.png

类型谓词

到目前为止,我们使用现有的 JavaScript 结构来处理类型范围缩小,但有时您希望更直接地控制类型。

这里可以用到一个返回类型为类型谓词的函数:

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


pet is Fish  是本例中的类型谓词。谓词采用  parameterName is Type  形式,其中  parameterName  是当前函数签名中参数的名称。

任何时候调用  isFish  时,TypeScript 都会将参数变量类型缩小为 Fish。

type Fish = { swim: () => void };
type Bird = { fly: () => void };
declare function getSmallPet(): Fish | Bird;
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}



// ---cut---
// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();


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

请注意,TypeScript 不仅知道 if 中的 pet 是 Fish 类型,它还知道 else 中的 pet 不是 Fish 类型,那么就一定是 Bird 类型。

还可以通过 isFish 来过滤出 Fish 类型数组:

const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()];
const underWater1: Fish[] = zoo.filter(isFish);
// or, equivalently
const underWater2: Fish[] = zoo.filter(isFish) as Fish[];


// The predicate may need repeating for more complex examples
const underWater3: Fish[] = zoo.filter((pet): pet is Fish => {
  if (pet.name === "sharkey") return false;
  return isFish(pet);
});

可区分联合

大多数时候在 JavaScript 中我们会处理稍微复杂的类型结构。

出于某种目的,假设我们正在尝试对圆形和正方形等形状进行编码。圆形记录它们的半径,正方形记录它们的边长。我们将使用一个名为  kind  的字段来判断我们正在处理的形状。我们尝试定义 Shape 类型。

interface Shape {
  kind: "circle" | "square";
  radius?: number;
  sideLength?: number;
}

我们使用字符串文字类型的联合: “circle” 和 “square” 来告诉我们应该将形状视为圆形还是正方形。通过使用 “circle” | “square” 而不是 string ,可以避免拼写错误问题。

image.png

我们编写一个  getArea  函数,根据它处理的是圆形还是方形来应用正确的逻辑。我们将首先尝试处理圆形。

image.png

在  strictNullChecks  下给我们一个错误,这是合理的,因为  radius  可能未定义。但是如果我们对  kind  属性执行适当的检查呢?

image.png

嗯,TypeScript 仍然不知道在这里做什么。但是我们已经达到了比类型检查器更了解值类型的地步。我们可以尝试使用非空断言( shape.radius  之后的  ! )来说明  radius  肯定存在。

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



}

但这并不理想。我们不得不用那些非空断言 ( ! ) 对类型检查器强制提醒,以说服它  shape.radius  已定义,但如果我们需要移动代码,这些断言很容易出错。此外,在  strictNullChecks  之外,我们无论如何都可以意外访问这些字段中的任何一个(因为可选属性只是假定在读取它们时始终存在)。我们可以做得更好。

当前 Shape  定义的问题是类型检查器无法根据  kind  属性知道是否存在  radius  或  sideLength 。我们需要将信息传达给类型检查器。考虑到这一点,我们重新定义  Shape 。

interface Circle {
  kind: "circle";
  radius: number;
}


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



type Shape = Circle | Square;

在这里,我们正确地将 Shape 分成了两种类型, kind 属性具有不同的值,但是 radius 和 sideLength 在它们各自的类型中被声明为必需的属性。

我们再次尝试检查  kind  属性

image.png

没有报错。

在这种情况下, kind  是公共属性(这被认为是  Shape  的判别属性)。检查  kind  属性是否为  "circle"  删除了  Shape  中不具有  "circle"  类型的  kind  属性的所有类型。这将  shape  缩小为  Circle  类型。

同样的检查也适用于  switch  语句。现在我们可以尝试编写完整的  getArea ,而不用任何讨厌的  !  非空断言。

image.png

这里最重要的是  Shape  的定义方式。向 TypeScript 传达了正确的信息( Circle  和  Square  实际上是具有特定  kind  字段的两个独立类型)。这样做可以让我们编写类型安全的 TypeScript 代码。

可区分联合不仅仅用于谈论圆和正方形。它们适用于在 JavaScript 中表示任何类型的消息传递方案,例如通过网络发送消息(客户端/服务器通信)。

never 类型

在类型范围缩小时,您可以将并集的选项减少到已经删除所有可能性并且什么都没有留下的程度。在这种情况下,TypeScript 将使用  never  类型来表示这种没有任何类型的类型。

详尽检查

never  类型可分配给每个类型;但是,没有类型可以分配给  never ( never  本身除外)。这意味着您可以使用范围类型缩小并依靠  never  出现在  switch  语句中进行详尽检查。

例如,向我们的  getArea  函数添加一个  default 分支,并分配  never  类型,那么未处理所有可能的情况时将引发错误。

image.png

将新成员添加到  Shape  联合会导致 TypeScript 错误:

image.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY1iPfIU' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片