TypeScript进阶
问题
你觉得使用 TS 的好处是什么?
TypeScript 是 JavaScript 的超集,它给 JavaScript 添加了可选的静态类型和基于类的面向对象编程,它拓展了 JavaScript 的语法
TypeScript 是面向对象的编程语言,包含类和接口的概念
TypeScript 开发时能给出编译错误,JavaScript 需要运行时暴露
TypeScript 为强类型语言,代码可读性强
TypeScript 添加很多方便的特性,比如可选链
type 和 interface 的异同?
- 相同点
- 都可以描述一个对象或者函数
- 都允许扩展 extends:interface 和 type 都可以拓展,并且两者并不是相互独立的,也就是说 interface 可以 extends type 也可以 extends interface
- 异同点
- type 可以声明基础类型别名、联合类型、元组等类型
- type 语句中还可以使用 typeof 获取实例的类型进行赋值
- interface 能够声明合并
什么是泛型?
泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性
好处:
- 增程序的可扩展性:函数或类可以很轻松地支持多种类型
- 增强代码的可读性:不必写多条函数重载,或者冗长的联合类型声明
- 灵活地控制类型之间的约束
基础
基础
特殊类型
- any 指的是一个任意类型,它是官方提供的一个选择性绕过静态类型检测的作弊方式
- unknown 是 TypeScript 3.0 中添加的一个类型,它主要用来描述类型并不确定的变量
- void 类型,它仅适用于表示没有返回值的函数
- undefined 的最大价值主要体现在接口类型上,它表示一个可缺省、未定义的属性
- null 的价值我认为主要体现在接口制定上,它表明对象或属性可能是空值
- never 表示永远不会发生值的类型
- object 类型表示非原始类型的类型,即非 number、string、boolean、bigint、symbol、null、undefined 的类型。然而,它也是个没有什么用武之地的类型
undefined 与 void
- 我们可以把 undefined 值或类型是 undefined 的变量赋值给 void 类型变量,反过来,类型是 void 但值是 undefined 的变量不能赋值给 undefined 类型
指鹿为马
- any 和 unknown 这两个特殊类型属于万金油,因为它们既可以被断言成任何类型,反过来任何类型也都可以被断言成 any 或 unknown
- 因此,如果我们想强行“指鹿为马”,就可以先把“鹿”断言为 any 或 unknown,然后再把 any 和 unknown 断言为“马”,比如鹿 as any as 马
类型拓宽与缩小
- Type Widening
- Type Narrowing
函数返回值
- 注意:TypeScript 3.6 之前的版本不支持指定 next、return 的类型
type AnyType = boolean
type AnyReturnType = string
type AnyNextType = number
function* gen(): Generator<AnyType, AnyReturnType, AnyNextType> {
const nextValue = yield true // nextValue 类型是 number,yield 后必须是 boolean 类型
return `${nextValue}` // 必须返回 string 类型
}
?:
表示参数可以缺省、可以不传。但是,如果我们声明了参数类型为xxx | undefined
,就表示函数参数是不可缺省且类型必须是 xxx 或者 undfined
类
- 抽象类,它是一种不能被实例化仅能被子类继承的特殊类
- 使用接口与使用抽象类相比,区别在于接口只能定义类成员的类型
interface IAdder {
x: number
y: number
add: () => number
}
class NumAdder implements IAdder {
x: number
y: number
constructor(x: number, y: number) {
this.x = x
this.y = y
}
add() {
return this.x + this.y
}
addTwice() {
return (this.x + this.y) * 2
}
}
Interface 与 Type 的区别
适用接口类型标注的地方大都可以使用类型别名进行替代,这是否意味着在相应的场景中这两者等价呢?
- 在大多数的情况下使用接口类型和类型别名的效果等价
- 在某些特定的场景下这两者还是存在很大区别。比如,重复定义的接口类型,它的属性会叠加,这个特性使得我们可以极其方便地对全局变量、第三方库的类型做扩展
高级类型
联合类型(Unions)用来表示变量、参数的类型不是单一原子类型,而可能是多种不同的类型的组合
- 使用
|
操作符分隔类型的语法来表示联合类型
交叉类型(Intersection Type),它可以把多个类型合并成一个类型,合并后的类型将拥有所有成员类型的特性
- 使用
&
操作符来声明交叉类型
类型缩减
TypeScript 如下的场景做了缩减,它把字面量类型、枚举成员类型缩减掉,只保留原始类型、枚举类型等父类型,这是合理的“优化”
type URStr = 'string' | string; // 类型是 string
// 可是这个缩减,却极大地削弱了 IDE 自动提示的能力
type BorderColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | string; // 类型缩减成 string
// 使用类型黑魔法
type BorderColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | string & {}; // 字面类型都被保留
枚举类型
数字类型
enum Day {
SUNDAY = 1,
MONDAY = 2
}
字符串类型
enum Day {
SUNDAY = 'SUNDAY',
MONDAY = 'MONDAY'
}
异构枚举
enum Day {
SUNDAY = 'SUNDAY',
MONDAY = 2
}
常量和计算成员
enum FileAccess {
// 常量成员
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// 计算成员
G = '123'.length
}
泛型
泛型指的是类型参数化,即将原来某种具体的类型进行参数化
- 和定义函数参数一样,我们可以给泛型定义若干个类型参数
- 并在调用时给泛型传入明确的类型参数
设计泛型的目的在于有效约束类型成员之间的关系,比如函数参数和返回值、类或者接口成员和方法之间的关系
对于 React 开发者而言,组件也支持泛型
function GenericCom<P>(props: { prop1: string }) {
return <></>;
};
<GenericCom<{ name: string }> prop1="1" ... />
在条件类型判断的情况下(比如上边示例中出现的 extends),如果入参是联合类型,则会被拆解成一个个独立的(原子)类型(成员)进行类型运算
function reflectSpecified<P extends number | string | boolean>(param: P): P {
return param
}
reflectSpecified('string')
reflectSpecified(1)
reflectSpecified(true)
注意:枚举类型不支持泛型
如何使用 TypeScript 实现 call?
- Parameters
- ReturnType
进阶
类型守卫
常用的类型守卫包括 switch、字面量恒等、typeof、instanceof、in 和自定义类型守卫这几种
类型兼容
any
any 类型可以赋值给除了 never 之外的任意其他类型,反过来其他类型也可以赋值给 any
unkonwn
不能把 unknown 赋值给除了 any 之外任何其他类型,反过来其他类型都可以赋值给 unknown
never
never 的特性是可以赋值给任何其他类型,但反过来不能被其他任何类型(包括 any 在内)赋值(即 never 是 bottom type)
void
void 类型仅可以赋值给 any 和 unknown 类型(下面示例第 9~10 行),反过来仅 any、never、undefined 可以赋值给 void
null、undefined
null、undefined 表现出与 void 类似的兼容性,即不能赋值给除 any 和 unknown 之外的其他类型,反过来其他类型(除了 any 和 never 之外)都不可以赋值给 null 或 undefined
增强类型
declare
- 使用 declare关键字时,我们不需要编写声明的变量、函数、类的具体实现(因为变量、函数、类在其他库中已经实现了),只需要声明其类型即可
- 在使用 TypeScript 开发前端应用时,我们可以通过 import 关键字导入文件,比如先使用 import 导入图片文件,再通过 webpack 等工具处理导入的文件。
- 但是,因为 TypeScript 并不知道我们通过 import 导入的文件是什么类型,所以需要使用 declare 声明导入的文件类
declare function toString(x: number): string
namespace
- 由于 ES6 后来也使用了 module 关键字,为了兼容 ES6,所以 TypeScript 使用 namespace 替代了原来的 module,并更名为命名空间
Definitely Typed 是最流行性的高质量 TypeScript 声明文件类库,正是因为有社区维护的这个声明文件类库,大大简化了 JavaScript 项目迁移 TypeScript 的难度
注意的是后面声明的接口具有更高的优先级
interface Obj {
identity(val: any): any
}
interface Obj {
identity(val: number): number
}
interface Obj {
identity(val: boolean): boolean
}
// 相当于
interface Obj {
identity(val: boolean): boolean
identity(val: number): number
identity(val: any): any
}
官方工具类型
操作接口类型
Partial
- Partial 工具类型可以将一个类型的所有属性变为可选的
type IPartial<T> = {
[P in keyof T]?: T[P]
}
Required
- 与 Partial 工具类型相反,Required 工具类型可以将给定类型的所有属性变为必填的
type IRequired<T> = {
[P in keyof T]-?: T[P]
}
Readonly
- Readonly 工具类型可以将给定类型的所有属性设为只读
type IReadonly<T> = {
readonly [P in keyof T]: T[P]
}
Pick
- Pick 工具类型可以从给定的类型中选取出指定的键值,然后组成一个新的类型
type IPick<T, K extends keyof T> = {
[P in K]: T[P]
}
Omit
- 与 Pick 类型相反,Omit 工具类型的功能是返回去除指定的键值之后返回的新类型
type IOmit<T, K extends keyof any> = IPick<T, Exclude<keyof T, K>>
联合类型
Exclude
- Exclude 的作用就是从联合类型中去除指定的类型
type IExclude<T, U> = T extends U ? never : T
Extract
- Extract 类型的作用与 Exclude 正好相反,Extract 主要用来从联合类型中提取指定的类型
type Extract<T, U> = T extends U ? T : never
NonNullable
- NonNullable 的作用是从联合类型中去除 null 或者 undefined 的类型
type INonNullable<T> = Exclude<T, null | undefined>
Record
- Record 的作用是生成接口类型,然后我们使用传入的泛型参数分别作为接口类型的属性和值
type IRecord<K extends keyof any, T> = {
[P in K]: T
}
在 TypeScript 中,keyof any 指代可以作为对象键的属性
- 目前,JavaScript 仅支持 string、number、symbol 作为对象的键值
函数类型
ConstructorParameters
- ConstructorParameters 可以用来获取构造函数的构造参数,而 ConstructorParameters 类型的实现则需要使用 infer 关键字推断构造参数的类型
- 关于 infer 关键字,我们可以把它当成简单的模式匹配来看待。如果真实的参数类型和 infer 匹配的一致,那么就返回匹配到的这个类型
type IConstructorParameters<T extends new (...args: any) => any> = T extends new (
...args: infer P
) => any
? P
: never
Parameters
- Parameters 的作用与 ConstructorParameters 类似,Parameters 可以用来获取函数的参数并返回序对
ReturnType
- ReturnType 的作用是用来获取函数的返回类型
ThisParameterType
- ThisParameterType 可以用来获取函数的 this 参数类型
ThisType
- ThisType 的作用是可以在对象字面量中指定 this 的类型
- 注意:如果你想使用这个工具类型,那么需要开启noImplicitThis的 TypeScript 配置
OmitThisParameter
- OmitThisParameter 工具类型主要用来去除函数类型中的 this 类型
- 如果传入的函数类型没有显式声明 this 类型,那么返回的仍是原来的函数类型
字符串类型
TypeScript 自 4.1版本起开始支持模板字符串字面量类型
基于 Exclude 工具类型的代码实现,请你分析一下为什么它可以从联合类型中排除掉指定成员
type IExclude<T, U> = T extends U ? never : T
类型物料
泛型:工具类型的本质就是构造复杂类型的泛型
- 使用泛型进行变量抽离、逻辑封装其实就是在造类型的轮子
infer(条件类型中的类型推断 )
- 可以在条件类型中使用类型推断操作符 infer 来获取入参的组成部分
- 比如说获取数组类型入参里元素的类型
type ElementTypeOfArray<T> = T extends (infer E)[] ? E : never
type isNumber = ElementTypeOfArray<number[]> // number
type isNever = ElementTypeOfArray<number> // never
type ElementTypeOfObj<T> = T extends { name: infer E; id: infer I } ? [E, I] : never
type isArray = ElementTypeOfObj<{ name: 'name'; id: 1; age: 30 }> // ['name', 1]
type isNever = ElementTypeOfObj<number> // never
keyof
- 使用 keyof 关键字提取对象属性名、索引名、索引签名的类型
typeof
- 如果我们在表达式上下文中使用 typeof,则是用来获取表达式值的类型
- 如果在类型上下文中使用,则是用来获取变量或者属性的类型
let StrA = 'a'
const unions = typeof StrA // "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
const str: typeof StrA = 'string' // string
type DerivedFromStrA = typeof StrA // string
in
- 我们可以使用索引签名语法和 in 关键字限定对象属性的范围
type SpecifiedKeys = 'id' | 'name'
type TargetType = {
[key in SpecifiedKeys]: any
} // { id: any; name: any; }
in 和 keyof 也只能在类型别名定义中组合使用
interface SourceInterface {
readonly id: number
name?: string
}
type TargetGenericType<S> = {
[key in keyof S]: S[key]
}
type TargetInstance = TargetGenericType<SourceInterface> // { readonly id: number; name?: string | undefined }
自 TypeScript 4.1 起,我们可以在映射类型的索引签名中使用类型断言
自己造轮子
ReturnTypeOfResolved 和官方 ReturnType 的区别:如果入参 F 的返回类型是泛型 Promise 的实例,则返回 Promise 接收的入参
type ReturnTypeOfResolved<F extends (...args: any) => any> = F extends (
...args: any[]
) => Promise<infer R>
? R
: ReturnType<F>
type isNumber = ReturnTypeOfResolved<() => number> // number
基于映射类型将类型入参 A 和 B 合并为一个类型的泛型 Merge<A, B>
type Merge<A, B> = {
[key in keyof A | keyof B]: key extends keyof A
? key extends keyof B
? A[key] | B[key]
: A[key]
: key extends keyof B
? B[key]
: never
}
type Merged = Merge<{ id: number; name: string }, { id: string; age: number }>
实战指南
tsconfig.json
compilerOptions
target
target 选项用来指定 TypeScript 编译代码的目标
module
module 选项可以用来设置 TypeScript 代码所使用的模块系统
jsx
jsx 选项用来控制 jsx 文件转译成 JavaScript 的输出方式
incremental
incremental 选项用来表示是否启动增量编译
declaration
declaration 选项用来表示是否为项目中的 TypeScript 或 JavaScript 文件生成 .d.ts 文件
sourceMap
sourceMap 选项用来表示是否生成sourcemap 文件,这些文件允许调试器和其他工具在使用实际生成的 JavaScript 文件时,显示原始的 TypeScript 代码
lib
安装 TypeScript 时会顺带安装一个 lib.d.ts 声明文件,并且默认包含了 ES5、DOM、WebWorker、ScriptHost 的库定
strict
开启 strict 选项时,一般我们会同时开启一系列的类型检查选项,以便更好地保证程序的正确性
在迁移 JavaScript 代码时,可以先暂时关闭一些严格模式的设置
alwaysStrict:
保证编译出的文件是 ECMAScript 的严格模式,并且每个文件的头部会添加 'use strict'
strictNullChecks
更严格地检查 null 和 undefined 类型,比如数组的 find 方法的返回类型将是更严格的 T | undefined
strictBindCallApply
更严格地检查 call、bind、apply 函数的调用,比如会检查参数的类型与函数类型是否一致
strictFunctionTypes
更严格地检查函数参数类型和类型兼容性
strictPropertyInitialization
更严格地检查类属性初始化,如果类的属性没有初始化,则会提示错误
noImplicitAny
禁止隐式 any 类型,需要显式指定类型。TypeScript 在不能根据上下文推断出类型时,会回退到 any 类型
noImplicitThis
禁止隐式 this 类型,需要显示指定 this 的类型
noImplicitReturns
禁止隐式返回。如果代码的逻辑分支中有返回,则所有的逻辑分支都应该有返回
noUnusedLocals
禁止未使用的本地变量。如果一个本地变量声明未被使用,则会抛出错误
noUnusedParameters
禁止未使用的函数参数。如果函数的参数未被使用,则会抛出错误。
noFallthroughCasesInSwitch
禁止 switch 语句中的穿透的情况。开启 noFallthroughCasesInSwitch 后,如果 switch 语句的流程分支中没有 break 或 return ,则会抛出错误,从而避免了意外的 swtich 判断穿透导致的问题
moduleResolution
moduleResolution 用来指定模块解析策略
baseUrl
baseUrl 指的是基准目录,用来设置解析非绝对路径模块名时的基准目录
paths
paths 指的是路径设置,用来将模块路径重新映射到相对于 baseUrl 定位的其他路径配置
rootDirs
rootDirs 可以指定多个目录作为根目录
typeRoots
typeRoots 用来指定类型文件的根目录
types
在默认情况下,所有的 typeRoots 包都将被包含在编译过程中
allowSyntheticDefaultImports
allowSyntheticDefaultImports 允许合成默认导出
esModuleInterop
esModuleInterop 指的是 ES 模块的互操作性
sourceRoot
sourceRoot 用来指定调试器需要定位的 TypeScript 文件位置,而不是相对于源文件的路径
mapRoot
mapRoot 用来指定调试器需要定位的 source map 文件的位置,而不是生成的文件位置
inlineSourceMap
开启 inlineSourceMap 选项时,将不会生成 .js.map 文件,而是将 source map 文件内容生成内联字符串写入对应的 .js 文件中
inlineSources
开启 inlineSources 选项时,将会把源文件的所有内容生成内联字符串并写入 source map 中
experimentalDecorators
experimentalDecorators 选项会开启装饰器提案的特性
skipLibCheck
开启 skipLibChec 选项,表示可以跳过检查声明文件
forceConsistentCasingInFileNames
TypeScript 对文件的大小写是敏感的
include
include 用来指定需要包括在 TypeScript 项目中的文件或者文件匹配路径
exclude 用来指定解析 include 配置中需要跳过的文件或者文件匹配路径
extends 配置项的值是一个字符串,用来声明当前配置需要继承的另外一个配置的路径,这个路径使用 Node.js 风格的解析模式
报错信息
1169
接口类型定义中由于使用了非字面量或者非唯一 symbol 类型作为属性名造成的
2322
ts(2322)是一个静态类型检查的错误,在注解的类型和赋值的类型不同的时候就会抛出这个错误
2339
在恒为 false 的类型守卫条件判断下,变量的类型将缩小为 never
- never 是所有其他类型的子类型,所以是类型缩小为 never,而不是变成 never
2345
传参时由于类型不兼容造成的
2352
TypeScript 类型收缩特性的 TS2352 类型错误
- 因为 setTimeout 的类型守卫失效,所以 x 的类型不会缩小为 string
let x: string | undefined
setTimeout(() => {
if (x) {
x.trim() // OK
}
})
2456
由于类型别名循环引用了自身造成的 TS2456 类型错误
2554
比较常见的一个 TS2554 错误,它是由于形参和实参个数不匹配造成的
2684
在 TypeScript 中,我们只需要在函数的第一个参数中声明 this 指代的对象(即函数被调用的方式)即可
- 如果我们直接调用 say(),this 实际上应该指向全局变量 window,但是因为 TypeScript 无法确定 say 函数被谁调用,所以将 this 的指向默认为 void,也会提示了一个 ts(2684)
- 此时,我们可以通过调用 window.say() 来避免这个错误,这也是一个安全的设计。因为在 JavaScript 的严格模式下,全局作用域函数中 this 的指向是 undefined
应用
Component
在 1.6 版本中,TypeScript 官方专门实现了对 React JSX 语法的静态类型支持,并在 tsconfig 中新增了一个 jsx 参数用来定制 JSX 的转译规则
Component 类型化的本质在于清晰地表达组件的属性、状态以及 JSX 元素的类型和结构
interface IEProps {
Cp?: React.ComponentClass<{ id?: number }>
}
interface IEState {
id: number
}
const ClassCp: React.ComponentClass<
IEProps,
IEState
> = class ClassCp extends React.Component<IEProps, IEState> {
public state: IEState = { id: 1 }
render() {
const { Cp } = this.props as Required<IEProps>
return <Cp id={`${this.state.id}`} /> // ts(2322)
}
static defaultProps: Partial<IEProps> = {
Cp: class extends React.Component {
render = () => null
}
}
}
遍历对象
在 TypeScript 里面,当遍历对象的时候会出现如下错误提示
function test (foo: object) {
for (let key in foo) {
console.log(foo[key]); // typescript错误提示
// do something
}
}
因为 foo 作为 object 没有声明 string 类型可用,所以 foo[key] 将会是 any 类型
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.
No index signature with a parameter of type 'string' was found on type '{}'.ts(7053)
解决方案:
把对象声明 as any
typescriptfunction test(foo: object) { for (const key in foo) { console.log((foo as any)[key]) // 报错消失 // do something } }
给对象声明一个接口
typescriptinterface StringKeyObject { [key: string]: any } function test(foo: StringKeyObject) { for (const key in foo) { console.log(foo[key]) // 报错消失 // do something } }
使用泛型
typescriptfunction test<T extends object>(foo: T) { for (const key in foo) { console.log(foo[key]) // 报错消失 // do something } }
使用 keyof
typescriptinterface Ifoo { name: string age: number weight: number } function test(opt: Ifoo) { let key: keyof Ifoo for (key in opt) { console.log(opt[key]) // 报错消失 // do something } }
类型缩减
TypeScript 如下的场景做了缩减,它把字面量类型、枚举成员类型缩减掉,只保留原始类型、枚举类型等父类型,这是合理的优化
type URStr = 'string' | string; // 类型是 string
// 可是这个缩减,却极大地削弱了 IDE 自动提示的能力
type BorderColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | string; // 类型缩减成 string
// 使用类型黑魔法
type BorderColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | string & {}; // 字面类型都被保留