-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
TypeScript 之 Narrowing #218
Comments
好家伙 你终于更新了 |
// both of these result in 'true'
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true, value: true
|
大佬回归! |
现在不卷了吗=。= |
哈哈,遇到老人了呀。第一卷就有你啦! |
垂死梦中惊坐起! |
@saltires 感谢不离不弃 |
@btea 你发现的不止是我的,还是官方文档的 bug |
@mqyqingfeng 哈哈,是这样吗?我还以为是你手误 |
@btea 快去给官方文档提 PR 吧 |
@jawil 实不相瞒,都被卷成咸鱼了,现在我要反卷回去了! |
@xiaoxin-sky 我们当时就是因为博客而认识,如今都已经过去六年,值得纪念 @jawil |
@huangsiyuan2015 垂死梦中惊坐起,缝缝补补又三年。咬定青山不放松,留取丹心照汗青。 |
好家伙,大佬终于更新了! |
@chenshaonian 接下来还会再多多更新几篇 |
强!👍🏻 |
非常详细,赞! |
@mqyqingfeng 这里似乎是正确的,相关人员给的回答解释 https://www.typescriptlang.org/play#example/type-widening-and-narrowing |
确实是的,但他们也没有明确的用 const 呀…… |
对,如果用 let ,此时推导的变量类型是 |
react呢!!!!等了一年了呜呜呜 |
@Baozhen-Yin 再等等,会有的 |
终于更新了,赞~ |
---------------------------------- 假冒课代表来更新下摘要, 如有错误,还望见谅指正 --------------------------------- narrowing
内容:
|
可惜ts没有模式匹配,不然会更好用。 |
@JararvisQ 因为没有课代表,所以你就是课代表了~ |
图片加载失败了 |
这里的 |
想请问下,如果传入的类型有多种不想在类型参数这里一个个写出来(dog | cat ...) ,但是又需要判断其中一部分特有的字段,有没有写法可以做到能让ts正确识别那些特有的属性。 |
没有填加 // |
可辨别联合(Discriminated unions)很有用,一直受困于这种情况,现在知道怎么处理了 |
牛逼 |
类型判断式这里补充一下翻译: |
有个书写错误: |
这个评论大佬可以分享一下怎么弄的嘛。。。。我也想要评论。。。 |
这一章一时半会儿感觉是学不明白了= =回头等深入使用以后再来看看。。 |
前言
TypeScript 的官方文档早已更新,但我能找到的中文文档都还停留在比较老的版本。所以对其中新增及修改较多的章节进行了个人的翻译整理。
本篇整理自 https://www.typescriptlang.org/docs/handbook/2/narrowing.html
本文并不完全遵循原文翻译,对部分内容自己也做了解释补充。
Narrowing
试想我们有这样一个函数,函数名为 padLeft:
该函数实现的功能是:
如果参数
padding
是一个数字,我们就在input
前面添加同等数量的空格,而如果padding
是一个字符串,我们就直接添加到input
前面。让我们实现一下这个逻辑:
如果这样写的话,编辑器里
padding + 1
这个地方就会标红,显示一个错误。
这是 TypeScript 在警告我们,如果把一个
number
类型 (即例子里的数字 1 )和一个number | string
类型相加,也许并不会达到我们想要的结果。换句话说,我们应该先检查下padding
是否是一个number
,或者处理下当padding
是string
的情况,那我们可以这样做:这个代码看上去也许没有什么有意思的地方,但实际上,TypeScript 在背后做了很多东西。
TypeScript 要学着分析这些使用了静态类型的值在运行时的具体类型。目前 TypeScript 已经实现了比如
if/else
、三元运算符、循环、真值检查等情况下的类型分析。在我们的
if
语句中,TypeScript 会认为typeof padding === number
是一种特殊形式的代码,我们称之为类型保护 (type guard),TypeScript 会沿着执行时可能的路径,分析值在给定的位置上最具体的类型。TypeScript 的类型检查器会考虑到这些类型保护和赋值语句,而这个将类型推导为更精确类型的过程,我们称之为收窄 (narrowing)。 在编辑器中,我们可以观察到类型的改变:
从上图中可以看到在
if
语句中,和剩余的return
语句中,padding
的类型都推导为更精确的类型。接下来,我们就介绍
narrowing
所涉及的各种内容。typeof 类型保护(type guards)
JavaScript 本身就提供了
typeof
操作符,可以返回运行时一个值的基本类型信息,会返回如下这些特定的字符串:typeof
操作符在很多 JavaScript 库中都有着广泛的应用,而 TypeScript 已经可以做到理解并在不同的分支中将类型收窄。
在 TypeScript 中,检查
typeof
返回的值就是一种类型保护。TypeScript 知道typeof
不同值的结果,它也能识别 JavaScript 中一些怪异的地方,就比如在上面的列表中,typeof
并没有返回字符串null
,看下面这个例子:在这个
printAll
函数中,我们尝试判断strs
是否是一个对象,原本的目的是判断它是否是一个数组类型,但是在 JavaScript 中,typeof null
也会返回object
。而这是 JavaScript 一个不幸的历史事故。熟练的用户自然不会感到惊讶,但也并不是所有人都如此熟练。不过幸运的是,TypeScript 会让我们知道
strs
被收窄为strings[] | null
,而不仅仅是string[]
。真值收窄(Truthiness narrowing)
在 JavaScript 中,我们可以在条件语句中使用任何表达式,比如
&&
、||
、!
等,举个例子,像if
语句就不需要条件的结果总是boolean
类型这是因为 JavaScript 会做隐式类型转换,像
0
、NaN
、""
、0n
、null
undefined
这些值都会被转为false
,其他的值则会被转为true
。当然你也可以使用
Boolean
函数强制转为boolean
值,或者使用更加简短的!!
:这种使用方式非常流行,尤其适用于防范
null
和undefiend
这种值的时候。举个例子,我们可以在printAll
函数中这样使用:可以看到通过这种方式,成功的去除了错误。
但还是要注意,在基本类型上的真值检查很容易导致错误,比如,如果我们这样写
printAll
函数:我们把原本函数体的内容包裹在一个
if (strs)
真值检查里,这里有一个问题,就是我们无法正确处理空字符串的情况。如果传入的是空字符串,真值检查判断为false
,就会进入错误的处理分支。如果你不熟悉 JavaScript ,你应该注意这种情况。
另外一个通过真值检查收窄类型的方式是通过
!
操作符。等值收窄(Equality narrowing)
Typescript 也会使用
switch
语句和等值检查比如==
!==
==
!=
去收窄类型。比如:在这个例子中,我们判断了
x
和y
是否完全相等,如果完全相等,那他们的类型肯定也完全相等。而string
类型就是x
和y
唯一可能的相同类型。所以在第一个分支里,x
和y
就一定是string
类型。
判断具体的字面量值也能让 TypeScript 正确的判断类型。在上一节真值收窄中,我们写下了一个没有正确处理空字符串情况的
printAll
函数,现在我们可以使用一个更具体的判断来排除掉null
的情况:JavaScript 的宽松相等操作符如
==
和!=
也可以正确的收窄。在 JavaScript 中,通过== null
这种方式并不能准确的判断出这个值就是null
,它也有可能是undefined
。对== undefined
也是一样,不过利用这点,我们可以方便的判断一个值既不是null
也不是undefined
:in 操作符收窄
JavaScript 中有一个
in
操作符可以判断一个对象是否有对应的属性名。TypeScript 也可以通过这个收窄类型。
举个例子,在
"value" in x
中,"value"
是一个字符串字面量,而x
是一个联合类型:通过
"swim" in animal
,我们可以准确的进行类型收窄。而如果有可选属性,比如一个人类既可以
swim
也可以fly
(借助装备),也能正确的显示出来:instanceof 收窄
instanceof
也是一种类型保护,TypeScript 也可以通过识别instanceof
正确的类型收窄:赋值语句(Assignments)
TypeScript 可以根据赋值语句的右值,正确的收窄左值。
注意这些赋值语句都有有效的,即便我们已经将
x
改为number
类型,但我们依然可以将其更改为string
类型,这是因为x
最初的声明为string | number
,赋值的时候只会根据正式的声明进行核对。
所以如果我们把
x
赋值给一个 boolean 类型,就会报错:控制流分析(Control flow analysis)
至此我们已经讲了 TypeScript 中一些基础的收窄类型的例子,现在我们看看在
if
while
等条件控制语句中的类型保护,举个例子:在第一个
if
语句里,因为有return
语句,TypeScript 就能通过代码分析,判断出在剩余的部分return padding + input
,如果 padding 是number
类型,是无法达到 (unreachable) 这里的,所以在剩余的部分,就会将number
类型从number | string
类型中删除掉。
这种基于可达性(reachability) 的代码分析就叫做控制流分析(control flow analysis)。在遇到类型保护和赋值语句的时候,TypeScript 就是使用这样的方式收窄类型。而使用这种方式,一个变量可以被观察到变为不同的类型:
类型判断式(type predicates)
在有的文档里,
type predicates
会被翻译为类型谓词。考虑到 predicate 作为动词还有表明、声明、断言的意思,区分于类型断言(Type Assertion),这里我就索性翻译成类型判断式。如果引用这段解释:
所谓
predicate
就是一个返回boolean
值的函数。
那我们接着往下看。
如果你想直接通过代码控制类型的改变, 你可以自定义一个类型保护。实现方式是定义一个函数,这个函数返回的类型是类型判断式,示例如下:
在这个例子中,
pet is Fish
就是我们的类型判断式,一个类型判断式采用parameterName is Type
的形式,但parameterName
必须是当前函数的参数名。
当 isFish 被传入变量进行调用,TypeScript 就可以将这个变量收窄到更具体的类型:
注意这里,TypeScript 并不仅仅知道
if
语句里的pet
是Fish
类型,也知道在else
分支里,pet
是Bird
类型,毕竟pet
就两个可能的类型。你也可以用
isFish
在Fish | Bird
的数组中,筛选获取只有Fish
类型的数组:可辨别联合(Discriminated unions)
让我们试想有这样一个处理
Shape
(比如Circles
、Squares
)的函数,Circles
会记录它的半径属性,Squares
会记录它的边长属性,我们使用一个kind
字段来区分判断处理的是Circles
还是Squares
,这是初始的Shape
定义:注意这里我们使用了一个联合类型,
"circle" | "square"
,使用这种方式,而不是一个string
,我们可以避免一些拼写错误的情况:现在我们写一个获取面积的
getArea
函数,而圆和正方形的计算面积的方式有所不同,我们先处理一下是Circle
的情况:在
strictNullChecks
模式下,TypeScript 会报错,毕竟radius
的值确实可能是undefined
,那如果我们根据kind
判断一下呢?你会发现,TypeScript 依然在报错,即便我们判断
kind
是circle
的情况,但由于radius
是一个可选属性,TypeScript 依然会认为radius
可能是undefined
。我们可以尝试用一个非空断言 (non-null assertion), 即在
shape.radius
加一个!
来表示radius
是一定存在的。但这并不是一个好方法,我们不得不用一个非空断言来让类型检查器确信此时
shape.raidus
是存在的,我们在 radius 定义的时候将其设为可选属性,但又在这里将其认为一定存在,前后语义也是不符合的。所以让我们想想如何才能更好的定义。
此时
Shape
的问题在于类型检查器并没有方法根据kind
属性判断radius
和sideLength
属性是否存在,而这点正是我们需要告诉类型检查器的,所以我们可以这样定义Shape
:在这里,我们把
Shape
根据kind
属性分成两个不同的类型,radius
和sideLength
在各自的类型中被定义为required
。
让我们看看如果直接获取
radius
会发生什么?就像我们第一次定义
Shape
那样,依然有错误。当最一开始定义
radius
是optional
的时候,我们会得到一个报错 (strickNullChecks
模式下),因为 TypeScript 并不能判断出这个属性是一定存在的。而现在报错,是因为
Shape
是一个联合类型,TypeScript 可以识别出shape
也可能是一个Square
,而Square
并没有radius
,所以会报错。但这时我们再根据
kind
属性检查一次呢?你会发现,报错就这样被去除了。
当联合类型中的每个类型,都包含了一个共同的字面量类型的属性,TypeScript 就会认为这是一个可辨别联合(discriminated union),然后可以将具体成员的类型进行收窄。
在这个例子中,
kind
就是这个公共的属性(作为 Shape 的可辨别(discriminant) 属性 )。这也适用于
switch
语句:这里的关键就在于如何定义
Shape
,告诉 TypeScript,Circle
和Square
是根据kind
字段彻底分开的两个类型。这样,类型系统就可以在switch
语句的每个分支里推导出正确的类型。可辨别联合的应用远不止这些,比如消息模式,比如客户端服务端的交互、又比如在状态管理框架中,都是很实用的。
试想在消息模式中,我们会监听和发送不同的事件,这些都是以名字进行区分,不同的事件还会携带不同的数据,这就应用到了可辨别联合。客户端与服务端的交互、状态管理,都是类似的。
never 类型
当进行收窄的时候,如果你把所有可能的类型都穷尽了,TypeScript 会使用一个
never
类型来表示一个不可能存在的状态。让我们接着往下看。
穷尽检查(Exhaustiveness checking)
never 类型可以赋值给任何类型,然而,没有类型可以赋值给
never
(除了never
自身)。这就意味着你可以在switch
语句中使用never
来做一个穷尽检查。举个例子,给
getArea
函数添加一个default
,把shape
赋值给never
类型,当出现还没有处理的分支情况时,never
就会发挥作用。当我们给
Shape
类型添加一个新成员,却没有做对应处理的时候,就会导致一个 TypeScript 错误:因为 TypeScript 的收窄特性,执行到
default
的时候,类型被收窄为Triangle
,但因为任何类型都不能赋值给never
类型,这就会产生一个编译错误。通过这种方式,你就可以确保getArea
函数总是穷尽了所有shape
的可能性。TypeScript 系列
TypeScript 系列文章由官方文档翻译、重难点解析、实战技巧三个部分组成,涵盖入门、进阶、实战,旨在为你提供一个系统学习 TS 的教程,全系列预计 40 篇左右。点此浏览全系列文章,并建议顺便收藏站点。
微信:「mqyqingfeng」,加我进冴羽唯一的读者群。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
The text was updated successfully, but these errors were encountered: