Blog icon indicating copy to clipboard operation
Blog copied to clipboard

TypeScript 之 Narrowing

Open mqyqingfeng opened this issue 2 years ago • 38 comments

前言

TypeScript 的官方文档早已更新,但我能找到的中文文档都还停留在比较老的版本。所以对其中新增及修改较多的章节进行了个人的翻译整理。

本篇整理自 https://www.typescriptlang.org/docs/handbook/2/narrowing.html

本文并不完全遵循原文翻译,对部分内容自己也做了解释补充。

Narrowing

试想我们有这样一个函数,函数名为 padLeft:

function padLeft(padding: number | string, input: string): string {
  throw new Error("Not implemented yet!");
}

该函数实现的功能是:

如果参数 padding 是一个数字,我们就在 input 前面添加同等数量的空格,而如果 padding 是一个字符串,我们就直接添加到 input 前面。

让我们实现一下这个逻辑:

function padLeft(padding: number | string, input: string) {
  return new Array(padding + 1).join(" ") + input;
	// Operator '+' cannot be applied to types 'string | number' and 'number'.
}

如果这样写的话,编辑器里 padding + 1 这个地方就会标红,显示一个错误。 ​ 这是 TypeScript 在警告我们,如果把一个 number 类型 (即例子里的数字 1 )和一个 number | string 类型相加,也许并不会达到我们想要的结果。换句话说,我们应该先检查下 padding 是否是一个 number,或者处理下当 paddingstring 的情况,那我们可以这样做:

function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    return new Array(padding + 1).join(" ") + input;
  }
  return padding + input;
}

这个代码看上去也许没有什么有意思的地方,但实际上,TypeScript 在背后做了很多东西。

TypeScript 要学着分析这些使用了静态类型的值在运行时的具体类型。目前 TypeScript 已经实现了比如 if/else 、三元运算符、循环、真值检查等情况下的类型分析。

在我们的 if 语句中,TypeScript 会认为 typeof padding === number 是一种特殊形式的代码,我们称之为类型保护 (type guard),TypeScript 会沿着执行时可能的路径,分析值在给定的位置上最具体的类型。

TypeScript 的类型检查器会考虑到这些类型保护和赋值语句,而这个将类型推导为更精确类型的过程,我们称之为收窄 (narrowing)。 在编辑器中,我们可以观察到类型的改变:

image.png

从上图中可以看到在 if 语句中,和剩余的 return 语句中,padding 的类型都推导为更精确的类型。

接下来,我们就介绍 narrowing 所涉及的各种内容。

typeof 类型保护(type guards)

JavaScript 本身就提供了 typeof 操作符,可以返回运行时一个值的基本类型信息,会返回如下这些特定的字符串:

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

typeof 操作符在很多 JavaScript 库中都有着广泛的应用,而 TypeScript 已经可以做到理解并在不同的分支中将类型收窄。 ​ 在 TypeScript 中,检查 typeof 返回的值就是一种类型保护。TypeScript 知道 typeof 不同值的结果,它也能识别 JavaScript 中一些怪异的地方,就比如在上面的列表中,typeof 并没有返回字符串 null,看下面这个例子:

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

在这个 printAll 函数中,我们尝试判断 strs 是否是一个对象,原本的目的是判断它是否是一个数组类型,但是在 JavaScript 中,typeof null 也会返回 object。而这是 JavaScript 一个不幸的历史事故。

熟练的用户自然不会感到惊讶,但也并不是所有人都如此熟练。不过幸运的是,TypeScript 会让我们知道 strs 被收窄为 strings[] | null ,而不仅仅是 string[]

真值收窄(Truthiness narrowing)

在 JavaScript 中,我们可以在条件语句中使用任何表达式,比如 &&||! 等,举个例子,像 if 语句就不需要条件的结果总是 boolean 类型

function getUsersOnlineMessage(numUsersOnline: number) {
  if (numUsersOnline) {
    return `There are ${numUsersOnline} online now!`;
  }
  return "Nobody's here. :(";
}

这是因为 JavaScript 会做隐式类型转换,像 0NaN""0nnull undefined 这些值都会被转为 false,其他的值则会被转为 true

当然你也可以使用 Boolean 函数强制转为 boolean 值,或者使用更加简短的!!

// both of these result in 'true'
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true,    value: true

这种使用方式非常流行,尤其适用于防范 nullundefiend 这种值的时候。举个例子,我们可以在 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);
  }
}

可以看到通过这种方式,成功的去除了错误。 ​ 但还是要注意,在基本类型上的真值检查很容易导致错误,比如,如果我们这样写 printAll 函数:

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

我们把原本函数体的内容包裹在一个 if (strs) 真值检查里,这里有一个问题,就是我们无法正确处理空字符串的情况。如果传入的是空字符串,真值检查判断为 false,就会进入错误的处理分支。

如果你不熟悉 JavaScript ,你应该注意这种情况。

另外一个通过真值检查收窄类型的方式是通过!操作符。

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 也会使用 switch 语句和等值检查比如 == !== == != 去收窄类型。比如:

image.png

在这个例子中,我们判断了 xy 是否完全相等,如果完全相等,那他们的类型肯定也完全相等。而 string 类型就是 xy 唯一可能的相同类型。所以在第一个分支里,xy 就一定是 string 类型。 ​ 判断具体的字面量值也能让 TypeScript 正确的判断类型。在上一节真值收窄中,我们写下了一个没有正确处理空字符串情况的 printAll 函数,现在我们可以使用一个更具体的判断来排除掉 null 的情况:

image.png

JavaScript 的宽松相等操作符如 ==!= 也可以正确的收窄。在 JavaScript 中,通过 == null 这种方式并不能准确的判断出这个值就是 null,它也有可能是 undefined 。对 == undefined 也是一样,不过利用这点,我们可以方便的判断一个值既不是 null 也不是 undefined

image.png

in 操作符收窄

JavaScript 中有一个 in 操作符可以判断一个对象是否有对应的属性名。TypeScript 也可以通过这个收窄类型。 ​ 举个例子,在 "value" in x 中,"value" 是一个字符串字面量,而 x 是一个联合类型:

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
}

通过 "swim" in animal ,我们可以准确的进行类型收窄。

而如果有可选属性,比如一个人类既可以 swim 也可以 fly (借助装备),也能正确的显示出来:

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 收窄

instanceof 也是一种类型保护,TypeScript 也可以通过识别 instanceof 正确的类型收窄:

image.png

赋值语句(Assignments)

TypeScript 可以根据赋值语句的右值,正确的收窄左值。

image.png

注意这些赋值语句都有有效的,即便我们已经将 x 改为 number 类型,但我们依然可以将其更改为 string 类型,这是因为 x 最初的声明为 string | number,赋值的时候只会根据正式的声明进行核对。 ​ 所以如果我们把 x 赋值给一个 boolean 类型,就会报错:

控制流分析(Control flow analysis)

至此我们已经讲了 TypeScript 中一些基础的收窄类型的例子,现在我们看看在 if while等条件控制语句中的类型保护,举个例子:

function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    return new Array(padding + 1).join(" ") + input;
  }
  return padding + input;
}

在第一个 if 语句里,因为有 return 语句,TypeScript 就能通过代码分析,判断出在剩余的部分 return padding + input ,如果 padding 是 number 类型,是无法达到 (unreachable) 这里的,所以在剩余的部分,就会将 number类型从 number | string 类型中删除掉。 ​ 这种基于可达性(reachability) 的代码分析就叫做控制流分析(control flow analysis)。在遇到类型保护和赋值语句的时候,TypeScript 就是使用这样的方式收窄类型。而使用这种方式,一个变量可以被观察到变为不同的类型:

image.png

类型判断式(type predicates)

在有的文档里, type predicates 会被翻译为类型谓词。考虑到 predicate 作为动词还有表明、声明、断言的意思,区分于类型断言(Type Assertion),这里我就索性翻译成类型判断式。

如果引用这段解释:

In mathematics, a predicate is commonly understood to be a Boolean-valued function_ P_: _X_→ {true, false}, called the predicate on X.

所谓 predicate 就是一个返回 boolean 值的函数。 ​ 那我们接着往下看。

如果你想直接通过代码控制类型的改变, 你可以自定义一个类型保护。实现方式是定义一个函数,这个函数返回的类型是类型判断式,示例如下:

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

在这个例子中,pet is Fish就是我们的类型判断式,一个类型判断式采用 parameterName is Type的形式,但 parameterName 必须是当前函数的参数名。 ​ 当 isFish 被传入变量进行调用,TypeScript 就可以将这个变量收窄到更具体的类型:

// 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
}

注意这里,TypeScript 并不仅仅知道 if 语句里的 petFish 类型,也知道在 else 分支里,petBird 类型,毕竟 pet 就两个可能的类型。

你也可以用 isFishFish | Bird 的数组中,筛选获取只有 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)

让我们试想有这样一个处理 Shape (比如 CirclesSquares )的函数,Circles 会记录它的半径属性,Squares 会记录它的边长属性,我们使用一个 kind 字段来区分判断处理的是 Circles 还是 Squares,这是初始的 Shape 定义:

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

注意这里我们使用了一个联合类型,"circle" | "square" ,使用这种方式,而不是一个 string,我们可以避免一些拼写错误的情况:

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.
    // ...
  }
}

现在我们写一个获取面积的 getArea 函数,而圆和正方形的计算面积的方式有所不同,我们先处理一下是 Circle 的情况:

function getArea(shape: Shape) {
  return Math.PI * shape.radius ** 2; // 圆的面积公式 S=πr²
  // Object is possibly 'undefined'.
}

strictNullChecks 模式下,TypeScript 会报错,毕竟 radius 的值确实可能是 undefined,那如果我们根据 kind 判断一下呢?

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
		// Object is possibly 'undefined'.
  }
}

你会发现,TypeScript 依然在报错,即便我们判断 kindcircle 的情况,但由于 radius 是一个可选属性,TypeScript 依然会认为 radius 可能是 undefined

我们可以尝试用一个非空断言 (non-null assertion), 即在 shape.radius 加一个 ! 来表示 radius 是一定存在的。

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

但这并不是一个好方法,我们不得不用一个非空断言来让类型检查器确信此时 shape.raidus 是存在的,我们在 radius 定义的时候将其设为可选属性,但又在这里将其认为一定存在,前后语义也是不符合的。所以让我们想想如何才能更好的定义。 ​ 此时 Shape的问题在于类型检查器并没有方法根据 kind 属性判断 radiussideLength 属性是否存在,而这点正是我们需要告诉类型检查器的,所以我们可以这样定义 Shape:

interface Circle {
  kind: "circle";
  radius: number;
}
 
interface Square {
  kind: "square";
  sideLength: number;
}
 
type Shape = Circle | Square;

在这里,我们把 Shape 根据 kind 属性分成两个不同的类型,radiussideLength 在各自的类型中被定义为 required。 ​ 让我们看看如果直接获取 radius 会发生什么?

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'.
}

就像我们第一次定义 Shape 那样,依然有错误。

当最一开始定义 radiusoptional 的时候,我们会得到一个报错 (strickNullChecks 模式下),因为 TypeScript 并不能判断出这个属性是一定存在的。

而现在报错,是因为 Shape 是一个联合类型,TypeScript 可以识别出 shape 也可能是一个 Square,而 Square 并没有 radius,所以会报错。

但这时我们再根据 kind 属性检查一次呢?

image.png

你会发现,报错就这样被去除了。 ​ 当联合类型中的每个类型,都包含了一个共同的字面量类型的属性,TypeScript 就会认为这是一个可辨别联合(discriminated union),然后可以将具体成员的类型进行收窄。

在这个例子中,kind 就是这个公共的属性(作为 Shape 的可辨别(discriminant) 属性 )。

这也适用于 switch 语句: image.png

这里的关键就在于如何定义 Shape,告诉 TypeScript,CircleSquare 是根据 kind 字段彻底分开的两个类型。这样,类型系统就可以在 switch 语句的每个分支里推导出正确的类型。

可辨别联合的应用远不止这些,比如消息模式,比如客户端服务端的交互、又比如在状态管理框架中,都是很实用的。

试想在消息模式中,我们会监听和发送不同的事件,这些都是以名字进行区分,不同的事件还会携带不同的数据,这就应用到了可辨别联合。客户端与服务端的交互、状态管理,都是类似的。

never 类型

当进行收窄的时候,如果你把所有可能的类型都穷尽了,TypeScript 会使用一个 never 类型来表示一个不可能存在的状态。

让我们接着往下看。

穷尽检查(Exhaustiveness checking)

​ never 类型可以赋值给任何类型,然而,没有类型可以赋值给 never (除了 never 自身)。这就意味着你可以在 switch 语句中使用 never 来做一个穷尽检查。

举个例子,给 getArea 函数添加一个 default,把 shape 赋值给 never 类型,当出现还没有处理的分支情况时,never 就会发挥作用。

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

当我们给 Shape 类型添加一个新成员,却没有做对应处理的时候,就会导致一个 TypeScript 错误:

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

因为 TypeScript 的收窄特性,执行到 default 的时候,类型被收窄为 Triangle,但因为任何类型都不能赋值给 never 类型,这就会产生一个编译错误。通过这种方式,你就可以确保 getArea 函数总是穷尽了所有 shape 的可能性。

TypeScript 系列

TypeScript 系列文章由官方文档翻译、重难点解析、实战技巧三个部分组成,涵盖入门、进阶、实战,旨在为你提供一个系统学习 TS 的教程,全系列预计 40 篇左右。点此浏览全系列文章,并建议顺便收藏站点。

微信:「mqyqingfeng」,加我进冴羽唯一的读者群。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

mqyqingfeng avatar Nov 12 '21 04:11 mqyqingfeng

好家伙 你终于更新了

saltires avatar Nov 12 '21 04:11 saltires

// both of these result in 'true'
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true,    value: true

!!"world" type 应该是 boolean

btea avatar Nov 12 '21 07:11 btea

大佬回归!

xiaoxin-sky avatar Nov 12 '21 07:11 xiaoxin-sky

现在不卷了吗=。=

jawil avatar Nov 12 '21 07:11 jawil

现在不卷了吗=。=

哈哈,遇到老人了呀。第一卷就有你啦!

xiaoxin-sky avatar Nov 12 '21 07:11 xiaoxin-sky

垂死梦中惊坐起!

huangsiyuan2015 avatar Nov 12 '21 13:11 huangsiyuan2015

好家伙 你终于更新了

@saltires 感谢不离不弃

mqyqingfeng avatar Nov 13 '21 02:11 mqyqingfeng

// both of these result in 'true'
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true,    value: true

!!"world" type 应该是 boolean

@btea 你发现的不止是我的,还是官方文档的 bug

mqyqingfeng avatar Nov 13 '21 02:11 mqyqingfeng

@mqyqingfeng 哈哈,是这样吗?我还以为是你手误

btea avatar Nov 13 '21 02:11 btea

@btea 快去给官方文档提 PR 吧

mqyqingfeng avatar Nov 13 '21 02:11 mqyqingfeng

现在不卷了吗=。=

@jawil 实不相瞒,都被卷成咸鱼了,现在我要反卷回去了!

mqyqingfeng avatar Nov 13 '21 02:11 mqyqingfeng

现在不卷了吗=。=

哈哈,遇到老人了呀。第一卷就有你啦!

@xiaoxin-sky 我们当时就是因为博客而认识,如今都已经过去六年,值得纪念 @jawil

mqyqingfeng avatar Nov 13 '21 02:11 mqyqingfeng

垂死梦中惊坐起!

@huangsiyuan2015 垂死梦中惊坐起,缝缝补补又三年。咬定青山不放松,留取丹心照汗青。

mqyqingfeng avatar Nov 13 '21 02:11 mqyqingfeng

好家伙,大佬终于更新了!

chenshaonian avatar Nov 13 '21 07:11 chenshaonian

好家伙,大佬终于更新了!

@chenshaonian 接下来还会再多多更新几篇

mqyqingfeng avatar Nov 14 '21 02:11 mqyqingfeng

强!👍🏻

wubaiqing avatar Nov 16 '21 01:11 wubaiqing

非常详细,赞!

NameWjp avatar Nov 17 '21 03:11 NameWjp

@btea 快去给官方文档提 PR 吧

@mqyqingfeng 这里似乎是正确的,相关人员给的回答解释 https://www.typescriptlang.org/play#example/type-widening-and-narrowing

btea avatar Nov 17 '21 03:11 btea

// both of these result in 'true'
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true,    value: true

!!"world" type 应该是 boolean

为啥我测试是boolean image

Ha0ran2001 avatar Nov 19 '21 13:11 Ha0ran2001

// both of these result in 'true'
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true,    value: true

!!"world" type 应该是 boolean

为啥我测试是boolean image

不是这个意思,ts 只是标注数据类型,并不改变数据类型,!!'a' 得到的值是 true , 数据类型当然是 boolean。上面说的类型是指 ts 根据值进行推导标记的类型,像这样

image

btea avatar Nov 19 '21 14:11 btea

@btea 快去给官方文档提 PR 吧

@mqyqingfeng 这里似乎是正确的,相关人员给的回答解释 https://www.typescriptlang.org/play#example/type-widening-and-narrowing

确实是的,但他们也没有明确的用 const 呀……

mqyqingfeng avatar Nov 21 '21 03:11 mqyqingfeng

@btea 快去给官方文档提 PR 吧

@mqyqingfeng 这里似乎是正确的,相关人员给的回答解释 https://www.typescriptlang.org/play#example/type-widening-and-narrowing

确实是的,但他们也没有明确的用 const 呀……

对,如果用 let ,此时推导的变量类型是 boolean, 要是再加一个 if 判断,此时变量类型就会被收窄,ts 推导得到一个 true 的类型。这篇文章讲的是类型收窄,所以在这个地方没有描述更加详细,所以才造成了我们的疑惑吧...

image

btea avatar Nov 21 '21 03:11 btea

react呢!!!!等了一年了呜呜呜

Baozhen-Yin avatar Nov 23 '21 03:11 Baozhen-Yin

react呢!!!!等了一年了呜呜呜

@Baozhen-Yin 再等等,会有的

mqyqingfeng avatar Nov 24 '21 01:11 mqyqingfeng

终于更新了,赞~

Lydever avatar Nov 24 '21 02:11 Lydever

---------------------------------- 假冒课代表来更新下摘要, 如有错误,还望见谅指正 ---------------------------------

narrowing

  • 是什么:TS的类型检查器会考虑到类型保护(typeof padding === number)和赋值语句,从而将类型推导为更精确类型的过程,称之为narrowing

内容:

  • typeof类型保护:typeof padding === number

  • 真值收窄:如果通过表达式判定为真值,则进行收窄。

  • 等值收窄:值相等,类型也完全相等

    • 同宽松相等(可用来:方便地判段两个值不是null也不是undefined
      if (value != null) {
        // no null and undefined
      }
      
  • in 操作符收窄:

    'swim' in animal
    
  • instanceOf收窄, 同typeOf

  • 赋值语句: 赋值语句的右值,正确的收窄左值

    let x:string | number
    x = 'cat' // x: string
    
  • 控制流分析: 不可达的代码不会进行类型分析。

    if (true) {
      x = 'Jess'
      return x
    }
    return x // NO
    
  • 类型判断式:类型的表明,声明,断言。

    cat as animal
    cat in animal
    
  • 可辨别联合:联合类型中的每个类型,包含了一个共同的字面量类型属性。TS会认为这是一个「可辨别联合」,对其进行收窄。

    interface cat {
      speak: 'miao'
    }
    interface dog {
      speak: 'wang'
    }
    
    switch(speak) {
      case 'wang':
        // ...
    }
    
  • never 穷尽检查:never可以赋值给任何类型,没有类型能赋值给never

    switch(type) {
      // ...
      default:
        const type:never = type
        // 如果没穷举完就会报错,可以用来判断是否穷尽了所有type的可能性
    }
    

Jess-2021 avatar Dec 04 '21 10:12 Jess-2021

可惜ts没有模式匹配,不然会更好用。

Salted-Fish-Swimming avatar Dec 22 '21 07:12 Salted-Fish-Swimming

@JararvisQ 因为没有课代表,所以你就是课代表了~

mqyqingfeng avatar Jan 04 '22 12:01 mqyqingfeng

图片加载失败了

shjames avatar May 25 '22 07:05 shjames

当最一开始定义 radius 是 optional 的时候,我们会得到一个报错 (strickNullChecks 模式下),
因为 TypeScript 并不能判断出这个属性是一定存在的。

这里的 strickNullChecks 应该为 strictNullChecks

xieziihang avatar Aug 10 '22 05:08 xieziihang