首页 前端 TypeScript 正文

TypeScript中你可能会忽略的细节

本文默认你已经在项目中实践了TypeScript(以下简称ts),本文不会讲解什么是ts,ts的具体语法等。本文只是对在使用ts开发项目中开发者可能会忽略的一些细节做一些总结。

回顾下TypeScript简单用法

let a: string = 'hello ts' // 显示注解声明
let b = 'hello ts' // ts自动推导
type A = string // 类型别名
interface Person { // 接口
    name: string
    age: number
    count: number
    friends: {
        name: string
        age: number
        address: string
    }[]
}
interface Shinji extends Person {
    Auto: string
}
enum Weekday { // 枚举
    Mon,
    Tue,
    Wed,
    ....
}
let tuple:[number, string] = [1, '2'] // 元组

interface Nerv {
    attack(target: string): void
}
class EVA implements Nerv { // 类与接口实现implements
    private readonly RobotType: string = 'Zero'
    attact(target: string) {
        console.log(target)
    }
}

那些你可能忽视的细节

const 声明的类型推导

ts 作为一种使用了两种类型系统实现的语言,既可以通过注解显示声明类型,也可以通过 ts 自动推导类型。请看下面例子,参数a的值通过ts类型推导,推导为 boolean 类型, 那么参数b ts 是否会自动推导出来是 boolean类型呢?

let a = true // boolean
const b = true // => ?

看一下结果:

很明显,参数 b 被自动推导为了 true 而不是 boolean,原因是因为,这里使用的是 const 而不是 let 或者 var。使用 const 声明的基本类型的值,赋值后无法修改,因此 ts 推导出的范围是最窄范围,也就是 c 的类型是 true 而不是 boolean

TypeScript中你可能会忽略的细节  第1张TypeScript中你可能会忽略的细节  第2张

那么我们再看一下下面这段 const 声明对象时的类型推导

const a = {
    b: 12
}

TypeScript中你可能会忽略的细节  第3张

这里很奇怪的是 ts 推导出来的 b 居然是一个 number 类型,而不是字面量12。上面第一个例子来说 const 或者 let 声明 是对ts推导有影响的,这些都是作用于基本类型,使用 const 声明对象的话,ts的推导就不会缩窄,这条规则也就失效了。这种情况类似 esnext 中的 const 声明。

索引签名

索引签名的句法为 [key: T]: U,我们通过索引签名告诉ts,指定的对象可能有更多的键。这种句法的意思是,“在这个对象中,类型为T的键对应的值的类型为U”。

需要注意的点:T的类型必须可以赋值给 number | string。当然key的值可以为任何词,不局限于非得用 'key'

元组

元组是 array 类型的子类型,是定义数组的特殊方式,长度固定,各索引上的值,具有固定的类型。

let a: [number] = [1]
let b : [number, string] = [1, '2']

元组可以支持可选属性

let c: [number, string?] = [1]

元组可以支持剩余参数即 ...rest 的形式

let d: [string, ...string[]] = ['shinji', 'rei', 'asuka']
let e: [number, boolean, ...string[]] = [3, true, 'hello', 'ts']

上面是元组的基本用法,但是 ts 实际在推导元组的时候会放宽要求,ts推导出来的类型尽可能的宽泛,而不是收窄,实际上做出来的推导并不会在乎元组的长度,以及元组所在索引值的类型,也就是放宽到元组会默认推导为数组,毕竟元组是 array 的子类型。看下面的代码示例:

let a = [1, true] // 没有显示注解声明,猜一下ts会自动推导出来什么?

揭晓答案:

浓眉大眼的居然 推导出来是 T[]的类型。

TypeScript中你可能会忽略的细节  第4张

如果我们想上面的代码就是要推导出为元组类型,有没有解决办法?有,当然有,一共有三种方式。

方式一:

let a = [1, true] as [number, boolean]

TypeScript中你可能会忽略的细节  第5张

方式二:

这种 as const 的方式有副作用,会把类型置为 readonly 并且类型变为const类型, 即 a 的实际类型为: readonly [1, true]

let a = [1, true] as const

TypeScript中你可能会忽略的细节  第6张

方式三:

我们可以使用 ts 推导剩余参数的类型方式,将元组的类型收窄

function tuple<T extends unknown[]>(...args: T): T {
  return args
}
let a = tuple(1, true)

TypeScript中你可能会忽略的细节  第7张

这里的关键在于 ...args: T ,由于范型 T描述的是剩余参数,因此 ts 推导出来是一个元组类型。

当项目中使用了大量的元组类型,但又不想使用ts默认的类型推导,我们就可以利用这个技术。

枚举

枚举的作用是列举类型中包含的各个值,枚举是一种无序的数据结构,把键映射到值上。

1个枚举可以分成多次声明,ts 会自动将其合并,和 interface 自动合并一个效果。

let a = true // boolean
const b = true // => ?0

ts 较为灵活,允许通过值访问枚举,也可以通过键访问枚举,但是容易出现两种问题

let a = true // boolean
const b = true // => ?1

我们来看下上面第二行代码的错误

TypeScript中你可能会忽略的细节  第8张

我们再来看一下 Language[6],WTF,langD 居然被推导为 string ,显然有问题啊。

TypeScript中你可能会忽略的细节  第9张

为了解决以上不安全的访问操作,我们可以通过 const enum 指定使用枚举的安全子集

let a = true // boolean
const b = true // => ?2

TypeScript中你可能会忽略的细节  第10张

使用安全子集的话该报错的就都会提示了,原因是:const enum 不允许反向查找,此行为与普通的JavaScript 对象很像。而且使用了 const enum 后,ts 是不会生成 js 代码的,此功能谨慎使用。如果想在使用 const enum 的同时又生成 js 代码,请在 ts.config.js 中开启 preserveConstEnums选项。

TS 中 this 类型注解

this 在 js 中是一个很特殊的存在,这里不过多的讨论 this 的用法,来聊一下 this 除了可以作为值

之外,还可以作为类型。

由于 this 调用方式的特殊性,在 ts 中我们有“保底”方案,对于函数来说,如果你定义的函数中使用了 this, 可以在函数的第一个参数中声明 this 的类型,这样每次调用函数的时候, ts 确保 this 的类型一定是你预期声明的类型,看代码:

let a = true // boolean
const b = true // => ?3

TypeScript中你可能会忽略的细节  第11张

对于类来说,this的类型也可以用于注解的方法返回类型。

我们实现一个 ES6 中的简易Set数据结构,首先看下 ES6 Set的用法

let a = true // boolean
const b = true // => ?4

我们同样用 ts 实现一个简易 Set(不做具体实现,只做类型说明)

let a = true // boolean
const b = true // => ?5

这样做是完全可以实现的,但是我们想要在定义 SimpleSet 的子类呢?

let a = true // boolean
const b = true // => ?6

由上面代码可以看到,每当我们扩展一个派生类的时候,都要把 add 方法返回的 this 签名给覆盖掉,比较麻烦。有没有更好的办法不这么麻烦?有。

我们可以使用 this 类型注解重写以下 SimpleSet 类:

let a = true // boolean
const b = true // => ?7

范型T

范型是一种多态类型参数,通常的使用方式是尖括号<T>,当然你也可以用其他字母等。

范型参数使用<>尖括号来声明,<>尖括号的主要作用就是<>所处的位置限定了范型的作用域,ts 会确保当前作用域中相同的范型会最终绑定为同一个具体的类型。看下代码

let a = true // boolean
const b = true // => ?8

那么范型是在什么时候被绑定具体的类型呢?声明范型的位置不仅限定范型的作用域,还决定了 ts 什么时候为范型确定绑定的具体类型

let a = true // boolean
const b = true // => ?9

一句话总结就是,ts 在使用范型时为范型绑定具体类型:对函数来说,在函数调用时绑定,对类来说,在类实例化时绑定,对类型别名和接口来说,在使用别名和实现接口时。

范型声明的位置?

看代码:

const a = {
    b: 12
}0

注意,在构造方法中不能声明范型。应该在类声明中声明范型 be like class Filter<T, U>

快速区分 void、any、unknown、never

简单的通过几个代码片段快速区分一下这几个类型,这里不再过多赘述。

const a = {
    b: 12
}1

快速了解型变

先看一小段代码:这里看 T extends U, 我们可以模拟的认为 T <: U,换句话来说,就是 T 类型是 U 类型的子类型,或者为同种类型。这样就一定能保证 T 类型 可以赋值给 U类型。

const a = {
    b: 12
}2

上面的代码就是 ts 中型变中的协变, 那么我们使用T <: U这种形式快速来了解下 ts 型变中的其他方式:

const a = {
    b: 12
}3

在 ts 中每个复杂类型的成员都会协变,包括对象、类、数组和函数。

函数的协变与其他类型的协变是稍有不同的,这里不多讨论。

函数类型重载

先来明确一下什么是重载函数,重载函数就是有多个调用签名的函数。先看下怎么写函数调用签名:

const a = {
    b: 12
}4

两种写法等效,只是使用方式不同而已。但是在函数类型重载的情况下,更推荐完整型的写法。

我们先看下一函数类型重载的例子:

const a = {
    b: 12
}5

运行上面这段代码我们会发现错误,错误如下:

TypeScript中你可能会忽略的细节  第12张

错误造成的原因来说一下,这是 ts 的调用签名重载机制造成的。如果为函数 reserve 声明多个重载的签名,在调用方看来,reserve 方法的类型是各个调用签名的并集。所以在实现 reserve 函数的时候,我们需要自己去声明组合后的调用签名,这是 ts 无法自动推导的。我们可以将 reserve 函数改为下面的方式就可以解决:

const a = {
    b: 12
}6

也就是说在手动实现函数的时候,要实现这两个调用签名的并集。

关于调用签名重载,可以查看浏览器DOM API, DOM API中有大量重载。

伴生对象模式

伴生对象模式应该是一个不太能常见到的概念,伴生对象模式来自 Scala, 目的是为了把同名的对象和类配对在一起。

了解伴生对象模式之前我们需要知道,在 ts 中的类型和值分别在不同的命名空间中的。这就意味着,在同一个作用域中可以存在同名的类型和值。比如在类中,类可以声明值也可以声明类型:

const a = {
    b: 12
}7

了解完上面的概念后,我们看下伴生对象模式:

const a = {
    b: 12
}8

使用以上伴生对象模式有几个好处:

  1. 可以语义上归属统一名称的类型和值

  2. 使用方可以一次性导入两者

除了可以将伴生对象模式用到值和类型别名外,接口和命名空间也可以使用伴生对象模式。

安全的扩展原型

由于 JS 是一门十分动态的语言,所以我们可以在 JS 运行时任意修改内置的方法,比如数组的push、Object.assign()等。所以对于 JS 来说,动态扩展远行是一种不安全的行为。但是有了 ts ,我们可以放心的扩展原型。

举个?:我们想给 Array 的原型添加一个 zip 方法,为了能够安全的扩展 Array 原型,我们需要做两件事:

  1. 比如在 zip.ts 文件中扩展 Array 的原型

  2. 新增 zip 方法,增强原型功能。

代码:

const a = {
    b: 12
}9

非空断言

我们在使用 ts 的时候经常会使用类型断言 a as string的形式来明确告诉 ts 这个就是我们预期的类型。那么什么是非空断言?

我们先看下那些类型可以为空, T | nullT | null | undefined,这是比较特殊的类型,在 ts 中专门为此提供语法,用于断定类型为 T 而不是null或者undefined

我们来看下代码:

let a: [number] = [1]
let b : [number, string] = [1, '2']0

上面代码其实会有两处错误,我截图标注出来

TypeScript中你可能会忽略的细节  第13张TypeScript中你可能会忽略的细节  第14张

我们先看下第一个错误,第一个错误,由于document.getElementById(dialog.id)处于一个新的作用域中,ts 并不知道代码会不会修改 dialog,所以此时 ts 的代码收窄不起作用,虽然dialog.id存在绝对可以确定 DOM 中有该 id 对应的元素,但是 ts 看来,调用 document.getElementById(dialog.id)返回的类型会是 HTMLElement | null

第二个错误就是,虽然我们一定知道element一定有父节点,但是 ts 依旧会推断 element.parentNode的类型是Node | null

解决方法当然有:

  1. 最暴力的就是 any 一把梭,我们可以类型断言为 any 即anything as any

  2. 可以使用大量的 if (_ === null)来进行判断,确保我们 ts 不会报错。

  3. 使用非空断言,代码如下:

let a: [number] = [1]
let b : [number, string] = [1, '2']1

使用非空断言的目的是为了明确的告诉 ts 我们确定 dialog.id、document.getElementById函数调用和 element.parentNode得到的结果是已经定义好的。这样 ts 就不会报错了。

与非空断言相反的应该就是 “明确赋值断言” ,这里不过多赘述。

模拟名义类型(隐含类型)

首先,我们需要知道的是 ts 是结构化的类型系统。但是我们可以通过 ts 来实现名义类型。

什么是名义类型?

首先让我们段代码了解一下什么是结构化类型系统

let a: [number] = [1]
let b : [number, string] = [1, '2']2

这时候名义类型就派出用场了!让我们通过代码来看一下怎么使用名义类型,名义类型在 ts 使用并不顺滑,但是在大型的项目或者大型的团队来说,名义类型能够更好的避免错误。

let a: [number] = [1]
let b : [number, string] = [1, '2']3

报错信息:

TypeScript中你可能会忽略的细节  第15张

虽然使用 string & {readonly brand: unique symbol}看起来很乱,但是没有其他更好的办法,这里使用了 unique symbol来作为唯一的 flag 的原因是因为在 ts 中实际上有两个真正意义上的名义类型,一个是unique symbol 另一个就是enum

辨别并集类型

我们先看一段代码:

let a: [number] = [1]
let b : [number, string] = [1, '2']4

为什么event.value 可以细化, 但是event.target 不能细化?因为handle函数的类型是 UserEvent ,但并不意味着一定传入 UserTextEvent 或者 UserMouseEvent 类型的值,甚至还可以传入两者的并集。由于并集类型的成员又可能重复,所以 ts 用了一种更稳妥的方式 明确了并集类型的具体情况。

如何解决这种 ts 无法细化并集的问题呢?看代码:

let a: [number] = [1]
let b : [number, string] = [1, '2']5

我们只需要一个字面量来标记并集类型的各种情况即可,但是这个字面量要满足以下几个:

  1. 在并集各个组成部分位置相同。

  2. 使用字面量类型(string, number, boolean等)。

  3. 不能使用范型。

  4. 要互斥,即在并集中是独一无二的。

非常好用的映射类型

ts 提供了非常强大好用的映射类型,比如内置的 Record 的实现。

let a: [number] = [1]
let b : [number, string] = [1, '2']6

既然提供了这么好用的映射类型,那我们看下映射类型能做什么?

let a: [number] = [1]
let b : [number, string] = [1, '2']7

说明一下:

  1. ?运算符可以将类型标记为可选的。

  2. readonly可以把类型标记为只读的。

  3. -运算符可以撤销?readonly-运算符需要置于 ?readonly之前。

键入运算符

键入运算符比较简单,和对象取值的操作类似,看一下代码就会明白:

let a: [number] = [1]
let b : [number, string] = [1, '2']8

infer R

infer R 属于条件类型的一种,是可以在条件中声明的范型。回顾一下我们以前使用范型是怎么用的?

type ElementType<T> = T extends unknown[] ? T[number] : T

。但是在条件类型中的声明,我们并不采用这种<T>尖括号的方式,我们使用 infer 关键字。

let a: [number] = [1]
let b : [number, string] = [1, '2']9

我们来看个稍微复杂的例子:

let c: [number, string?] = [1]0

从某种意义上来说 infer R 等同于范型声明,即 infer R == T, infer R 属于行内声明。

原文:https://juejin.cn/post/7111351347114410020
打赏
海报

本文转载自互联网,旨在分享有价值的内容,文章如有侵权请联系删除,部分文章如未署名作者来源请联系我们及时备注,感谢您的支持。

转载请注明本文地址:https://www.shouxicto.com/article/6124.html

相关推荐

前端代码用什么软件?

前端代码用什么软件?

           本文默认你已经在项目中实践了TypeScript(以下简称ts),本文不会讲解什么是ts,ts的具体语法等。本...

TypeScript 2023.03.30 0 9

typescript的书?

typescript的书?

           本文默认你已经在项目中实践了TypeScript(以下简称ts),本文不会讲解什么是ts,ts的具体语法等。本...

TypeScript 2023.03.29 0 20

ios12越狱(ios12越狱工具)

ios12越狱(ios12越狱工具)

           本文默认你已经在项目中实践了TypeScript(以下简称ts),本文不会讲解什么是ts,ts的具体语法等。本...

TypeScript 2023.03.29 0 18

支付宝
微信
赞助本站