共计 13646 个字符,预计需要花费 35 分钟才能阅读完成。
本文旨在总结 TypeScript
的体系化常识,帮忙你理解并相熟 TypeScript
的各项个性
什么是 TypeScript
TypeScript
是 JavaScript
的超集,通过增加动态类型定义与查看来对 JavaScript
进行扩大,TypeScript
与 JavaScript
的关系相似 Less/Sass
与 Css
为什么须要 TypeScript
JavaScript
是弱类型语言,很多谬误会在运行时才被发现,而 TypeScript
提供的动态类型查看能够帮忙开发者防止大部分运行时的谬误,并且可能大大加强代码的可维护性。相应的,付出的代价则是开发阶段须要书写相干的类型,老本方面有肯定的晋升
Playground
TypeScript
官网提供了一个在线的 TypeScript
开发环境 Playground,你能够很不便地在下面进行 TypeScript
的相干练习,反对配置 tsconfig
,动态类型检测以及 TypeScript
代码编译执行等
- Playground
原始数据类型
在 TypeScript
中,对于 JavaScript
中的原始数据类型都有对应的类型:
- string
- number
- boolean
- undefined
- null
- symbol
- bigint
e.g.
const str: string = 'text'
const num: number = 1
const bool: boolean = true
const undef: undefined = undefined
const null: null = null
const symb: symbol = Symbol('symb')
const bigint: bigint = BigInt(9007199254740993)
object
object
示意所有的非原始类型,包含数组、对象、函数等
e.g.
let demo: object
demo = []
demo = {}
demo = () => {}
demo = 1 // Error: Type 'number' is not assignable to type 'object'
Object 与 {}
在 JavaScript
里 Object
是所有原型链的最上层,在 TypeScript
里则体现为 Object
能够示意所有的类型,而 {}
均示意所有非 null
和 undefined
的类型,null
和 undefined
在 strictNullChecks=false
时才容许被赋值给 Object
和 {}
let demo1: Object
demo1 = []
demo1 = {}
demo1 = 1
demo1 = null // Error: Type 'null' is not assignable to type 'Object'
demo1 = undefined // Error: Type 'undefined' is not assignable to type 'Object'
let demo2: {}
demo2 = []
demo2 = {}
demo2 = 1
demo2 = null // Error: Type 'null' is not assignable to type '{}'
demo2 = undefined // Error: Type 'undefined' is not assignable to type '{}'
应用倡议:
- 在任何时候都不要应用
Object
及相似的装箱类型- 防止应用
{}
,它示意任何非null/undefined
的值,与any
相似- 对于无奈确定类型,但能够确定不为原始类型的,能够应用
object
— 更举荐应用具体的形容:Record<string, any>
或者unknown[]
等
其余类型
数组
数组定义有两种形式:
const arr: string[] = []
// 等价于
const arr: Array<string> = []
元组
数组合并了雷同的类型,元组则合并不同的类型:
const tup: [string, number] = ['LiHua', 18]
元祖中的选项还能够是可选的
// 反对可选
const tup1: [string, number?] = ['LiHua']
// 反对对属性命名
const tup2: [name: string, age?: number] = ['LiHua']
// 一个 react useState 的例子
const [state, setState] = useState();
函数
函数定义形式能够是以下几种:
// 函数式申明
function test1(x: number, y: number): number {return x + y}
// 表达式申明
const test2: (x: number, y: number) => number = (x, y) => {return x + y}
// 或
const test3 = (x: number, y: number): number => {return x + y}
void
在 JavaScript
中,void
作为立刻执行的函数表达式,用于获取 undefined
:
// 返回 undefined
void 0
// 等价于
void(0)
在 TypeScript
中则形容了一个函数没有显示返回值时的类型,例如上面这几种状况都能够用 void 来形容:
// case 1
function test1() {}
// case 2
function test2() {return;}
// case 3
function test3() {return undefined;}
any 与 unknown
- any: 示意任意类型,且不会对其进行类型推断和类型校验
- unknown: 示意一个未知的类型,会有肯定的类型校验
区别
-
任意类型都能赋值给
any
,any
也能够赋值给任意类型;任意类型都能赋值给unknown
,然而unknown
只能赋值给unknown/any
类型:let type1: any // 被任意类型赋值 type1 = 1 // 赋值给任意类型 let type2: number = type1 let type3: unknown // 被任意类型赋值 type3 = 1 // 赋值给任意类型 let type4: number = type3 // Error: Type 'unknown' is not assignable to type 'number'
-
unknown 在不进行类型推断的时候,无奈间接应用;any 则没有这个限度
let str1: unknown = 'string'; str1.slice(0, 1) // Error: Object is of type 'unknown'. let str2: any = 'string'; str2.slice(0, 1) // Success
增加类型推断后则能够失常应用:
let str: unknown = 'string';
// 1. 通过 as 类型断言
(str as string).slice(0, 1)
// 2. 通过 typeof 类型推断
if (typeof str === 'string') {str.slice(0, 1)
}
滥用 any 的一些场景以及应用倡议:
- 类型不兼容时应用
any
:举荐应用as
进行类型断言- 类型太简单不想写应用
any
:举荐应用as
进行类型断言,找到你所须要的最小单元- 不分明具体类型是什么而应用
any
:举荐申明时应用unknown
来代替,在具体调用的中央再进行断言
never
示意不存在的类型,个别在抛出异样以及呈现死循环的时候会呈现:
// 1. 抛出异样
function test1(): never {throw new Error('err')
}
// 2. 死循环
function test2(): never {while(true) {}}
never
也存在被动的应用场景,比方咱们能够进行具体的类型查看,对穷举之后剩下的 else 条件分支中的变量设置类型为 never
,这样一旦 value
产生了类型变动,而没有更新相应的类型判断的逻辑,则会产生报错提醒
const checkValueType = (value: string | number) => {if (typeof value === 'string') {// do something} else if (typeof value === 'number') {// do something} else {
const check: never = value
// do something
}
}
例如这里 value 产生类型变动而没有做对应解决,此时 else 里的 value
则会被收窄为 boolean
,无奈赋值给 never
类型,导致报错,这样能够确保解决逻辑总是穷举了 value
的类型:
const checkValueType = (value: string | number | boolean) => {if (typeof value === 'string') {// do something} else if (typeof value === 'number') {// do something} else {
const check: never = value // Error: Type 'boolean' is not assignable to type 'never'.
// do something
}
}
字面量类型
指定具体的值作为类型,个别与联结类型一起应用:
const num_literal: 1 | 2 = 1
const str_literal: "text1" | "text2" = "text1"
枚举
枚举应用 enum
关键字来申明:
enum TestEnum {
key1 = 'value1',
key2 = 2
}
JavaScript
对象是单向映射,而对于 TypeScript
中的枚举,字符串类型是单向映射,数字类型则是双向映射的,下面的枚举编译成 JavaScript
会被转换成如下内容:
"use strict";
var TestEnum;
(function (TestEnum) {TestEnum["key1"] = "value1";
TestEnum[TestEnum["key2"] = 2] = "key2";
})(TestEnum || (TestEnum = {}));
对于数字类型的枚举,相当于执行了 obj[k] = v
和 obj[v] = k
,以此来实现双向映射
常量枚举
应用 const
定义,与一般枚举的区别次要在于不会生成下面的辅助函数 TestEnum
,编译产物只有 const val = 2
const enum TestEnum {
key1 = 'value1',
key2 = 2
}
const val = TestEnum.key2
接口
接口 interface
是对行为的形象,TypeScript
里罕用来对对象进行形容
可选
可选属性,通过 ?
将该属性标记为可选
interface Person {
name: string
addr?: string
}
readonly
只读属性,对于对象润饰对象的属性为只读;对于 数组 / 元组 只能将整个 数组 / 元组 标记为只读
interface Person {
name: string
readonly age: number
}
const person: Person = {name: 'LiHua', age: 18}
person.age = 20 // Cannot assign to 'age' because it is a read-only property
const list: readonly number[] = [1, 2]
list.push(3) // Property 'push' does not exist on type 'readonly number[]'.
list[0] = 2 // Index signature in type 'readonly number[]' only permits reading
类型别名
类型别名次要利用 type
关键字,来用于对一组特定类型进行封装,咱们在 TypeScript
里的类型编程以及各种类型体操都离不开类型别名
type Person = {
name: string;
readonly age: number;
addr?: string;
}
Interface 与 type 的异同点
相同点:
-
都能够用来定义对象,都能够实现扩大
type Person = {name: string} // 接口通过继承的形式实现类型扩大:interface Person1 extends Person {age: number} // 类型别名通过穿插类型的形式实现类型扩大:type Person2 = Person & {age: number}
不同点:
-
type
能够用来定义原始类型、联结 / 穿插类型、元组等,interface
则不行type str = string type num = number type union = string | number type tup = [string, number]
-
interface
申明的同名类型能够进行合并,而type
则不能够,会报标识符反复的谬误interface Person1 {name: string} interface Person1 {age: string} let person: Person1 // {name: string; age: string} type Person2 {name: string} // Error: Duplicate identifier 'Person2' type Person2 {age: string}
-
interface
会有索引签名的问题,而type
没有interface Test1 {name: string} type Test2 = {name: string} const data1: Test1 = {name: 'name1'} const data2: Test2 = {name: 'name2'} interface PropType {[key: string]: string } let prop: PropType prop = data1 // Error: Type 'Test2' is not assignable to type 'PropType'. Index signature for type 'string' is missing in type 'Test2' prop = data2 // success
因为只有当该类型的所有属性都已知并且能够对照该索引签名进行查看时,才容许将子集调配给该索引签名类型。而
interface
容许类型合并,所以它的最终类型是不确定的,并不一定是它定义时的类型;type
申明的类型时的索引签名是已知的
倡议:
官网举荐应用
interface
,当interface
无奈满足,例如须要定义联结类型等,再抉择应用type
TypeScript/type-aliases
联结类型与穿插类型
联结类型
示意一组可用的类型汇合,只有属于其中之一就属于这个联结类型
const union: string | number = 'text'
穿插类型
示意一组类型的叠加,须要满足所有条件才能够属于这个穿插类型,个别用于接口的合并
interface A {field1: string}
interface B {field2: number}
const test: A & B = {field1: 'text', field2: 1}
如果新的类型不可能存在,则会被转换为 never
,例如这里的 number & string
:
type A = number
type B = string
type Union = A & B // never
对于对象类型的穿插类型,会依照同名属性进行穿插,例如上面的 common
须要即蕴含 fieldA
也蕴含 fieldB
:
interface A {
field1: string
common: {fieldA: string}
}
interface B {
field2: number
common: {fieldB: number}
}
const fields: A & B = {
field1: 'text1',
field2: 1,
common: {fieldA: 'text2', fieldB: 2}
}
// success
如何绕过类型检测
鸭子类型
“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就能够被称为鸭子。”
鸭子类型放在 TypeScript
里来说就是咱们能够在鸟上构建走路、游泳、叫等办法,创立一只像鸭子的鸟,来绕开对鸭子的类型检测
e.g.
interface Param {field1: string}
const func = (param: Param) => param
func({field1: '111', field2: 2}) // Error
const param1 = {field1: '111', field2: 2}
func(param1) // success
在这里咱们结构了一个函数 func
承受参数为 Param
,当咱们间接调用 func
传参时,相当于是赋值给变量 param
,此时会严格依照参数校验进行,因而会报错;
而如果咱们应用一个变量存储,再将变量传递给 func
,此时则会利用鸭子类型的个性,因为 param1
中 蕴含 field1
,TypeScript
会认为 param1
曾经齐全实现了 Param
,能够认为 param1
对应的类型是 Param
的子类,这个时候则能够绕开对多余的 field2
的检测
类型断言
类型断言也能够绕过类型检测,下面的例子能够改成用类型断言来实现:
interface Param {field1: string}
const func = (param: Param) => param
func({field1: '111', field2: 2} as Param) // success
另外一种断言形式是非空断言,利用 !
关键词,能够从类型中排除 undefined
和 null
:
const func = (str: string) => str
const param = ['text1', 'text2'].find(str => str === 'text1')
func(param) // Error
func(param!) // success
泛型
泛型是一种形象类型,只有在调用时才晓得具体的类型。如果将类型类比为函数,那么泛型就相当于函数中的参数了
// 定义
type Test<T> = T | string;
// 应用
const test: Test<number> = 1
// react 中的例子
const [state, setState] = useState<number>(0)
函数中定义泛型
// 函数式申明
function func<T>(param: T): T {return param;}
// 表达式申明
const func: <T>(param: T) => T = (param) => {return param;}
类型操作符
在 TypeScript
中,能够通过类型操作符来对类型进行操作,基于已有的类型创立新的类型,次要包含以下几种:
typeof
typeof
能够获取变量或者属性对应的类型,返回的是一个 TypeScript 类型:
const str = 'text'
type Str = typeof str // string
对于对象类型的变量,则会保留键名,返回推断失去的键值的类型:
const obj = {
field1: 'text',
field2: 1,
field3: {field: 'text'}
}
type ObjType = typeof obj
// {
// field1: string;
// field2: number;
// field3: {
// field: string;
// };
// }
留神:
如果你为变量指定了相应的类型,例如
any
,那么typeof
将会间接返回你预约义的类型而不会进行类型推断
keyof
keyof
用于获取类型中所有的键,返回一个联结类型:
interface Test {
field1: string;
field2: number;
}
type Fields = keyof Test
// "field1" | "field2"
in
in
用于遍历类型,它是 JavaScript
里已有的概念:
type Fields = 'field1' | 'field2'
type Test = {[key in Fields]: string
}
// Test: {field1: string; field2: string}
extends
extends
用于对泛型增加束缚,使得泛型必须继承这些类型,例如这里要求泛型 T
必须要属于 string
或者 number
:
type Test<T extends string | number> = T[]
type TestExtends1 = Test<string> // success
type TestExtends2 = Test<boolean> // Type 'boolean' does not satisfy the constraint 'string | number'.
extends
还能够在条件判断语句中应用:
type Test<T> = T extends string | number ? T[] : T
type TestExtends1 = Test<string> // string[]
type TestExtends2 = Test<boolean> // boolean
infer
infer
次要用于申明一个待推断的类型,只能联合 extends
在条件判断语句中应用,咱们以内置的工具类 ReturnType
为例,它次要作用是返回一个函数返回值的类型,这里用 infer
示意待推断的函数返回值类型:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
索引类型与映射类型
索引类型
这里申明了一个蕴含索引签名且键为 string
的类型:
interface Test {[key: string]: string;
}
蕴含索引签名时,其余具体键的类型也须要合乎索引签名申明的类型:
interface Test {
// Error: Property 'field' of type 'number' is not assignable to 'string' index type 'string'
field: number;
[key: string]: string;
}
获取索引类型,通过 keyof
关键字,返回一个由索引组成的联结类型:
interface Test {
field1: string;
field2: number;
}
type Fields = keyof Test
// "field1" | "field2"
拜访索引类型,通过拜访键的类型,来获取对应的索引签名的类型:
interface Test {
field1: string;
field2: number
}
type Field1 = Test["field1"] // string
type Field2 = Test["field2"] // number
// 配合 keyof,能够获取索引签名对应类型的联结类型
type Fields = Test[keyof Test] // string | number
留神:
这里的 field1/field2 不是字符串,而是字面量类型
因而咱们还能够通过键的类型来拜访:
interface Test {[key: string]: number;
}
type Field = Test[string] // number
映射类型
与索引类型经常搭配应用的是映射类型,次要概念是依据键名映射失去键值类型,从旧的类型生成新的类型。咱们利用 in
联合 keyof
来对泛型的键进行遍历,即可失去一个映射类型,很多 TypeScript 内置的工具类的实现都离不开映射类型。
以实现一个简略的 ToString
,能将接口中的所有类型映射为 string
类型为例:
type ToString<T> = {[key in keyof T]: string
}
interface Test {
field1: string;
field2: number;
field3: boolean;
}
type Fields = ToString<Test>
工具类型
这里咱们列举了一些 TypeScript
内置的常用工具链的具体实现:
Partial
将所有属性变为可选,首先通过 in
配合 keyof
遍历 T
的所有属性赋值给 P
,而后配合 ?
将属性变为可选,最初 T[P]
以及 undefined
作为返回类型:
type Partial<T> = {[P in keyof T]?: T[P] | undefined;
}
应用示例:
interface Person {
name: string;
age?: number;
}
type PersonPartial = Partial<Person>
// {name?: string | undefined; age?: number | undefined}
Partial
只能将最外层的属性变为可选,相似浅拷贝,如果要想把深层地将所有属都变成可选,能够手动实现一下:
type DeepPartial<T> = {[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P] | undefined
}
Required
将所有属性变为必选,与 Partial
实现的思路相似,只不过变成了通过 -?
来去除可选符号:
type Required<T> = {[P in keyof T]-?: T[P];
}
应用示例:
interface Person {
name: string;
age?: number;
}
type PersonRequired = Required<Person>
// {name: string; age: number}
Readonly
将所有属性都变成只读,不可批改,与 Partial
实现的思路相似,利用 readonly
关键字来标识:
type Readonly<T> = {readonly [P in keyof T]: T[P];
}
应用示例:
interface Person {
name: string;
age?: number;
}
type PersonReadonly = Readonly<Person>
// {readonly name: string; readonly age?: number | undefined}
Record
以指定的类型生成对应类型的键值对,例如咱们常常会应用 Record<string, unknown>
或者 Record<string, any>
来对对象的类型进行申明,这里次要通过 K extends string | number | symbol
来限度 K
必须合乎索引的类型:
type Record<K extends string | number | symbol, T> = {[P in K]: T;
}
Exclude
移除属于指定类型的局部,通过判断如果 T
继承自 U
,那么返回 never
,则会移除 T
中属于 U
的类型:
type Exclude<T, U> = T extends U ? never : T
应用示例:
type Test = string | number
type TestExclude = Exclude<Test, string> // number
Extract
保留属于指定类型的局部,与 Exclude
逻辑绝对应,在这里则指保留 T
中属于 U
的类型:
type Extract<T, U> = T extends U ? T : never
应用示例:
type Test = string | number
type TestExtract = Extract<Test, string> // string
NonNullable
去除类型中的 null
和 undefined
:
type NonNullable<T> = T extends null | undefined ? never : T
应用示例:
type Test = string | number | null | undefined
type TestNonNullable = NonNullable<Test> // string | number
Pick
以选中的属性生成新的类型,相似 lodash.pick
,这里首先通过 extends
配置 keyof
获取到 T
中的所有子类型并赋值给 K
,当 P
属于 K
中的属性时,返回 T
对应的类型 T[P]
:
type Pick<T, K extends keyof T> = {[P in K]: T[P];
}
应用示例:
interface Person {
name: string;
age?: number;
}
type PersonPick = Pick<Person, 'age'>
// {age?: number}
Omit
排除选中的属性,以残余的属性生成新的类型,与 Pick
作用刚好相同,相似 lodash.omit
,这里首先通过 Exclude<keyof T, K>
来去除掉 T
中蕴含的属性 K
,而后当 P
属于该去除后的类型时,返回 T
对应的类型 T[P]
:
type Omit<T, K extends string | number | symbol> = {[P in Exclude<keyof T, K>]: T[P];
}
应用示例:
interface Person {
name: string;
age?: number;
}
type PersonOmit = Omit<Person, 'name'>
// {age?: number}
Parameters
取得函数参数的类型,返回一个元组,这里首先通过扩大运算法,将泛型函数中的参数通过 infer
定义为 P
,而后判断 T
是否合乎函数的类型定义,如果是则返回 P
:
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never
应用示例:
type Func = (param: string) => string[]
type FuncParam = Parameters<Func> // [param: string]
ReturnType
获取函数返回值的类型,实现与 Parameters
相似,将定义的类型从函数参数调整为函数的返回值类型:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
type Func = (param: string) => string[]
type FuncReturn = ReturnType<Func> // string[]
tsconfig
tsconfig
是 TypeScript
的我的项目配置文件,通过它你能够配置 TypeScript
的各种类型查看以及编译选项,这里次要介绍一些罕用的 compilerOptions
选项:
// tsconfig.json
{
"compilerOptions": {
/* 构建、工程化选项 */
// baseUrl: 解析的根目录
"baseUrl": "src",
// target: 编译代码到指标 ECMAScript 版本,个别是 es5/es6
"target": "es5",
// lib: 运行时环境反对的语法,默认与 tagert 的值相关联
"lib": ["dom", "es5", "es6", "esnext"],
// module: 编译产物对应的模块化规范,罕用值包含 commonjs/es6/esnext 等
"module": "esnext",
// moduleResolution: 模块解析策略,反对 node/classic,后者根本不举荐应用
"moduleResolution": "node",
// allowJs:是否容许引入 .js 文件
"allowJs": true,
// checkJs: 是否查看 .js 文件中的谬误
"checkJs": true,
// declaration: 是否生成对应的 .d.ts 类型文件,个别作为 npm 包提供时须要开启
"declaration": false,
// sourceMap: 是否生成对应的 .map 文件
"sourceMap": true,
// noEmit: 是否将构建产物写入文件系统,一个常见的实际是只用 tsc 进行类型查看,应用独自的打包工具进行打包
"noEmit": true,
// jsx: 如何解决 .tsx 文件中对于 jsx 的生成,罕用值包含:react/preserve
// 具体比对:https://www.typescriptlang.org/tsconfig#jsx
"jsx": "preserve",
// esModuleInterop: 开启后会生成辅助函数以兼容解决在 esm 中导入 cjs 的状况
"esModuleInterop": true,
// allowSyntheticDefaultImports: 在 cjs 没有默认导出时进行兼容,配合 esModuleInterop 应用
"allowSyntheticDefaultImports": true,
// forceConsistentCasingInFileNames: 是否强制导入文件时与系统文件的大小写统一
"forceConsistentCasingInFileNames": true,
// resolveJsonModule:是否反对导入 json 文件,并做类型推导和查看
"resolveJsonModule": true,
// experimentalDecorators: 是否反对装璜器实验性语法
"experimentalDecorators": true,
/* 类型查看选项 */
// strict: 是否启动严格的类型查看,蕴含一系列选项:https://www.typescriptlang.org/tsconfig#strict
"strict": true,
// skipLibCheck: 是否跳过非源代码中所有类型申明文件(.d.ts)的查看
"skipLibCheck": true,
// strictNullChecks: 是否启用严格的 null 查看
"strictNullChecks": true,
// noImplicitAny: 蕴含隐式 any 申明时是否报错
"noImplicitAny": true,
// noImplicitReturns: 是否要求所有函数执行门路中都有返回值
"noImplicitReturns": true,
// noUnusedLocals: 存在未应用的变量时是否报错
"noUnusedLocals": false,
// noUnusedParameters: 存在未应用的参数时是否报错
"noUnusedParameters": false,
}
}
对于
- 残缺的示例代码能够参考:blog-samples/typescript
- 本文首发于 github 和 集体博客,欢送关注和 star