本文旨在总结 TypeScript 的体系化常识,帮忙你理解并相熟 TypeScript 的各项个性

什么是 TypeScript

TypeScriptJavaScript 的超集,通过增加动态类型定义与查看来对 JavaScript 进行扩大,TypeScriptJavaScript 的关系相似 Less/SassCss

为什么须要 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 = 1const bool: boolean = trueconst undef: undefined = undefinedconst null: null = nullconst 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 与 {}

JavaScriptObject 是所有原型链的最上层,在 TypeScript 里则体现为 Object 能够示意所有的类型, 而 {} 均示意所有非 nullundefined 的类型,nullundefinedstrictNullChecks=false 时才容许被赋值给 Object{}

let demo1: Objectdemo1 = []demo1 = {}demo1 = 1demo1 = 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 = 1demo2 = null // Error: Type 'null' is not assignable to type '{}'demo2 = undefined // Error: Type 'undefined' is not assignable to type '{}'

应用倡议:

  1. 在任何时候都不要应用 Object 及相似的装箱类型
  2. 防止应用 {},它示意任何非 null/undefined 的值,与 any 相似
  3. 对于无奈确定类型,但能够确定不为原始类型的,能够应用 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

// 返回 undefinedvoid 0 // 等价于 void(0)

TypeScript 中则形容了一个函数没有显示返回值时的类型,例如上面这几种状况都能够用 void 来形容:

// case 1function test1() {}// case 2function test2() {    return;}// case 3function test3() {    return undefined;}

any 与 unknown

  • any: 示意任意类型,且不会对其进行类型推断和类型校验
  • unknown: 示意一个未知的类型,会有肯定的类型校验

区别

  1. 任意类型都能赋值给 anyany 也能够赋值给任意类型;任意类型都能赋值给 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'
  2. 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 的一些场景以及应用倡议:

  1. 类型不兼容时应用 any:举荐应用 as 进行类型断言
  2. 类型太简单不想写应用 any:举荐应用 as 进行类型断言,找到你所须要的最小单元
  3. 不分明具体类型是什么而应用 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 = 1const 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] = vobj[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 propertyconst 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}

不同点:

  1. type 能够用来定义原始类型、联结/穿插类型、元组等,interface 则不行

    type str = stringtype num = numbertype union = string | numbertype tup = [string, number]
  2. 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}
  3. 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: PropTypeprop = 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 = numbertype B = stringtype 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) => paramfunc({ field1: '111', field2: 2 }) // Errorconst param1 = { field1: '111', field2: 2 }func(param1) // success

在这里咱们结构了一个函数 func 承受参数为 Param ,当咱们间接调用 func 传参时,相当于是赋值给变量 param,此时会严格依照参数校验进行,因而会报错;

而如果咱们应用一个变量存储,再将变量传递给 func,此时则会利用鸭子类型的个性,因为 param1 中 蕴含 field1TypeScript 会认为 param1 曾经齐全实现了 Param ,能够认为 param1 对应的类型是 Param 的子类,这个时候则能够绕开对多余的 field2 的检测

类型断言

类型断言也能够绕过类型检测,下面的例子能够改成用类型断言来实现:

interface Param {    field1: string}const func = (param: Param) => paramfunc({ field1: '111', field2: 2 } as Param) // success

另外一种断言形式是非空断言,利用 ! 关键词,能够从类型中排除 undefinednull

const func = (str: string) => strconst param = ['text1', 'text2'].find(str => str === 'text1')func(param) // Errorfunc(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> // successtype TestExtends2 = Test<boolean> // Type 'boolean' does not satisfy the constraint 'string | number'.

extends 还能够在条件判断语句中应用:

type Test<T> = T extends string | number ? T[] : Ttype 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"] // stringtype 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 | numbertype TestExclude = Exclude<Test, string> // number

Extract

保留属于指定类型的局部,与 Exclude 逻辑绝对应,在这里则指保留 T 中属于 U 的类型:

type Extract<T, U> = T extends U ? T : never

应用示例:

type Test = string | numbertype TestExtract = Extract<Test, string> // string

NonNullable

去除类型中的 nullundefined

type NonNullable<T> = T extends null | undefined ? never : T

应用示例:

type Test = string | number | null | undefinedtype 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

tsconfigTypeScript 的我的项目配置文件,通过它你能够配置 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