TypeScrip从入门到进阶

注:本文大量引用了其他文章的精华之处,如果有冒犯还请见谅。

TS 诞生背景

JavaScript 的设计,只用了十天,且是为了应付公司的任务(设计师本人并没有兴趣)。初衷是为了实现一些简单的网页交互,并没有考虑设计为些大型应用的语言,因此存在很多不合理的设计。而 js 发展速度又很快,过早的标准化导致 js 的很多不合理设计被保留且不可能被更正。

语言类型按照【类型检查的时机】可以分为动态类型和静态类型,静态类型是在编译时就检查类型,动态类型则是在运行时确定类型;按照【是否允许隐式类型转换】分为强类型和弱类型,强类型一旦声明类型便不可变更,弱类型则存在隐式类型转换。

JS 被设计为动态弱类型语言,其变量类型是在运行时动态确定的(动态类型),并且可以随时改变(弱类型)。正因如此,JS 巨量错误都只能在运行时发现,在开发时容易写出难以觉察的错误,维护时又常发现难以理解的代码,极其不利于开发维护大型复杂应用。

为了解决 js 的类型安全和可维护性问题,业界做了很多尝试。最终,微软内部孵化并开源的 TypeScript 脱颖而出,并迅速统一了 js 的类型标准。

TS 对 JS 的类型增强

  • 类型系统按照「类型检查的时机」来分类,可以分为动态类型和静态类型。js 是动态类型:
js 复制代码
let foo = 1;

foo.split(' ');

上述代码在 js 中,只有运行时才会报错。但在 ts 中,编译阶段就会报错。因为 ts 中变量 foo 在声明时就被自动推断为 number 类型,而 number 类型上面并没有 split 方法。

  • 类型系统按照「是否允许隐式类型转换」来分类,可以分为强类型和弱类型。js 是弱类型:
js 复制代码
function sum(a, b){
  return a + b
}

sum(100, 100) // 200
sum(100, '100') // '100100'

上述代码在 js 中运行并不会报错,但是运算明显不符合我们的预期。在 js 中,函数 sum 的入参可以是任意类型,而操作费 + 号的作用也可以有多种,既可以用于数组的相加,还可以用于字符串的拼接,还可以用于字符串的强制类型转换(真灵活啊)。因此不同的如此会产生不同的结果(不符合预期)。

ts 复制代码
function sum(a: number, b: number){
  return a + b
}

sum(100, 100) // 200
sum(100, '100') // Argument of type 'string' is not assignable to parameter of type 'number'.

在ts中给函数 sum 入参增加类型后,在编译阶段便可发现语法错误。

可以发现 ts 弥补了 js 的类型检查和类型安全问题,可以极大提高开发过程中的编码正确率。

TS 对 JS 的编辑器增强

得益于 ts 的静态类型检查,使其可以增强编辑器(IDE)的功能,提供代码补全、接口提示、跳转到定义、代码重构更牢靠等能力。

代码提示:

接口提示:

TS 对 JS 的语法增强

一个语法进入到 Stage 3 阶段后,TypeScript 就会实现它。而不用等待漫长的正式标准出台。

class:

ts 复制代码
class Animal {
  public name;
  private age = 17;
  protected constructor(name) {
    this.name = name;
  }
}
class Cat extends Animal {
  constructor(name) {
    super(name);
    this.age // 编辑器提示:age是父类私有属性,无法访问
  }
}

let a = new Animal('Jack');
// index.ts(13,9): TS2674: Constructor of class 'Animal' is protected and only accessible within the class declaration.

public private protected 为 ts 中类的语法,编译成 js 后并不存在,且访问也并不受限。但在 ts 编码过程中需要遵循 ts 语法。

装饰器:

ts 复制代码
function sealed(constructor: Function) {
  Object.seal(constructor); // 不能添加新属性、不能删除现有属性等
  Object.seal(constructor.prototype);
}

@sealed
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}

上例装饰器 sealed 将 class 的构造函数和原型封闭起来。

TS 基础

TS 数据类型

  • 基本类型
ts 复制代码
let a: string = 'asdf'
let b: number = 1
let c: boolean = true
let d: void = undefined
let d: () => void = () => {}
let e: undefined = undefined
let f: null = null

基本类型可以完全自动推导,所以其实不需要额外写类型。

  • 字符串字面量类型
ts 复制代码
let str: 'small' | 'big' | 'fly'
str = 'small'
str = 'big'
str = 'fly'
  • 任意类型
ts 复制代码
let a: any // 任意类型
a = 1
a = 'asdf'
a = {}

可以看到变量被赋予 any 类型之后,就失去了使用 ts 的意义,重新回归弱类型了。

在日常编码中,要尽量避免使用 any 。

  • never 类型

不存在的类型,多用于条件判断。

ts 复制代码
type Extract<T, U> = T extends U ? T : never

由于 never 是三元运算的假值时的结果,导致该类型只返回判断条件为真的结果。

  • 类型断言

将类型断言为另一个类型

ts 复制代码
let a = {} as { a: string; b: number }
a = {
  a: 'asdf',
  b: 1,
}
  • 联合类型

表示值可以是多种类型中的一个

ts 复制代码
// 联合类型
let a: string | number = 1

a = 'asdf'
a.toString() // 联合类型只能使用其公共属性/方法

a = 1
;(a as unknown as string).length  // 或者使用断言

// 不建议使用接口的联合类型
let b: {a: string} | {b: number}
b = {a: 'asdf'}
b = {b: 1}
b.a // 报错
b.b // 报错
  • 交叉类型

交叉类型即同时满足多个类型约束

ts 复制代码
// 交叉类型不可用于基本类型
let a: string & number // a 的实际类型为 never,因为 a 不可能即使 string 又是 number

let b: {a: number} & {b: string} = { a: 1, b: 'asdf' } // b 的实际类型为 {a: number; b: string}
  • 数组类型

数组类型有两种书写方式

ts 复制代码
let a: string[] = ['1', '2', '3', ...]

let b: Array<number> = [1, 2, 3, ...]

// 数组泛型可以传入联合类型、接口等
let c: Array<string | number> = [1, 'a', 2, 'b', 'c', ...]
  • 元组

元组可以理解为更精确的数组

ts 复制代码
let a: [string, number] = ['a', 1]
a[0].length
a[1].toFixed()
  • 函数
ts 复制代码
let fn = () => {} // 自动推导 () => void

let add2 = (a: number) => a + 2 // (a: number) => number
  • 对象的类型------接口
ts 复制代码
interface IResponse {
  code: number
  data: any
  msg: string
}
let res: IResponse = {
  code: 200,
  data: true,
  msg: '成功',
}
  • 泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

ts 复制代码
interface IResponse<T> {
  code: number
  msg: string
  data: T
}
let res: IResponse<boolean> = {
  code: 200,
  data: true,
  msg: '成功',
}
  • 枚举
ts 复制代码
enum Status {
  SUCCESS = 200,
  FAIL = 0,
  NOTFOUND = 404,
}
if (code === Status.SUCCESS) {...}

TS 关键词特性

typeof 获取js变量的ts类型

js 中的 typeof 获取的是变量的 js 类型

ts 中的 typeof 获取的是变量的 ts 类型

ts 复制代码
let fn = () => {}
type Fn = typeof fn // 等同于 type Fn = () => void

keyof 索引查询和访问

keyof T 的结果为类型 T 上所有公有属性key的联合:

ts 复制代码
interface Eg1 {
  name: string,
  readonly age: number,
}
type T1 = keyof Eg1 // T1的类型实则是name | age
type T2 = Eg1[keyof Eg1] // string | number

in 属性遍历

in 用来遍历联合类型,用于遍历对象类型的属性

ts 复制代码
type P<T> = {
  [K in keyof T]: T[K]
}

extends 继承条件判断

  • 用于接口继承
ts 复制代码
interface T1 {
  name: string,
}
interface T2 {
  sex: number,
}

interface T3 extends T1, T2 {
  age: number,
}
// T3 = {name: string, sex: number, age: number}
  • 用于三元运算中做条件判断
ts 复制代码
type P1<T> = T extends string | number ? string : boolean
P<string> // string
P<number> // string
P<void> // boolean

extends前面的类型是泛型,且泛型传入的是联合类型时,则会依次判断该联合类型的所有子类型是否可分配给extends后面的类型(即可分裂性)。

ts 复制代码
// extends前非泛型
type A2 = 'x' | 'y' extends 'x' ? 1 : 2; // A2结果为 2

// extends前为泛型,且传入是联合类型
type P<T> = T extends 'x' ? 1 : 2;
type A3 = P<'x' | 'y'> // A3结果为 1

infer 推断为

infer用于extends的条件类型中让Ts自己推到类型。

ts 复制代码
type P<T extends Function> = T extends (...args: infer U) => any ? U : never

let a: P<(a: string, b: number) => void> // [a: string, b: number]

泛型 P 的参数是个函数,通过 infer 将该函数参数推断为 U,并返回此类型。

infer 只能用于条件句中的extends之后、? 之前

类型兼容性

在类型系统中,判断是父类型还是子类型,只需要记住:父类型更宽泛

下面我们基于类型的可赋值性、协变、逆变、双向协变等进行进一步的讲解。

  • 可赋值性

父类型可以被(子类型)赋值。

在接口中:

ts 复制代码
interface Animal {
  name: string;
}
interface Dog extends Animal {
  break(): void;
}
// 相对Dog,Animal更宽泛,是父类型。

let animal: Animal;
let dog: Dog;

animal = dog; // 可以赋值,子类型更佳具体,可以赋值给更佳宽泛的父类型
dog = animal; // 不可以赋值

在联合类型中:

ts 复制代码
type A = 1 | 2 | 3;
type B = 2 | 3;
// 相对B,A更宽泛,是父类型。
let a: A;
let b: B;

b = a; // 不可赋值
a = b; // 可以赋值
  • 协变

父子类型通过某种构造关系构造成的新的类型,如果还具有父子关系则是协变。

ts 复制代码
interface Animal {
  name: string;
}
interface Dog extends Animal {
  break(): void;
}

let Eg1: Animal;
let Eg2: Dog;
Eg1 = Eg2; // 兼容,可以赋值

let Eg3: Array<Animal>
let Eg4: Array<Dog>
Eg3 = Eg4 // 兼容,可以赋值

通过Eg3Eg4来看,在AnimalDog在变成数组后,Array<Dog>依旧可以赋值给Array<Animal>,这就是协变的。

  • 逆变

父子类型通过某种构造关系构造成的新的类型,如果父子关系逆转了则是逆变。

ts 复制代码
interface Animal {
  name: string;
}
interface Dog extends Animal {
  break(): void;
}
type AnimalFn = (arg: Animal) => void
type DogFn = (arg: Dog) => void

let animal: AnimalFn = (arg: Animal) => {}
let dog: DogFn = (arg: Dog) => { arg.break(); }
animal = dog; // 不可赋值
dog = animal; // 可以赋值

animal = dog 后其类型仍是 AnimalFn(假设不报错),在调用 animal({ name: 'cat' }) 时就会缺少参数break,而 ts 类型系统并不会报错;

dog = animal 后其类型仍是 DogFn,在调用 dog({ name: 'cat', break(){} }) 时多了参数break,这无所谓。

所以牢记函数参数处于逆变位置哦。

  • infer 的协变逆变

infer推导的名称相同并且都处于逆变 的位置,则推导的结果将会是交叉类型

ts 复制代码
type Bar<T> = T extends {
  a: (x: infer U) => void;
  b: (x: infer U) => void;
} ? U : never;

// type T1 = string
type T1 = Bar<{ a: (x: string) => void; b: (x: string) => void }>;

// type T2 = never
type T2 = Bar<{ a: (x: string) => void; b: (x: number) => void }>;

infer推导的名称相同并且都处于协变 的位置,则推导的结果将会是联合类型

ts 复制代码
type Foo<T> = T extends {
  a: infer U;
  b: infer U;
} ? U : never;

// type T1 = string
type T1 = Foo<{ a: string; b: string }>;

// type T2 = string | number
type T2 = Foo<{ a: string; b: number }>;

TS 进阶

内置类型工具解析

Partial

Partial<T>T的所有属性变成可选的。

ts 复制代码
type Partial<T> = {
  [K in keyof T]?: T[K]
}
  • [K in keyof T] 遍历所有属性
  • ?: 将属性变为可选
  • T[K] 设置为原来的类型

扩展:如何实现指定字段变为可选,剩余不变?

ts 复制代码
type PartialOption<T, U extends keyof T> = Omit<T, U> & Partial<Pick<T, U>>

Required

Required<T>T的所有属性变成必选的。

ts 复制代码
type Required<T> = {
  [P in keyof T]-?: T[P]
}

- 用于移除特定修饰符的功能,如 ? readonly

Readonly

Readonly<T>T的所有属性变成只读的。

ts 复制代码
type Readonly<T> = {
  readonly [K in keyof T]: T[K]
}

Pick

挑选一组属性并组成一个新的类型。

ts 复制代码
type Pick<T, P extends keyof T> = {
  [K in P]: T[K]
}

Record

构造一个typekey为联合类型中的每个子类型,类型为T

ts 复制代码
type Record2<U extends keyof any, T> = {
  [K in U]: T
}

值得注意的是keyof any得到的是string | number | symbol

Exclude

Exclude<T, U>提取存在于T,但不存在于U的类型组成的联合类型。

ts 复制代码
type Exclude<T, U> = T extends U ? never : T

Extract

Extract<T, U>提取联合类型T和联合类型U的所有交集。

ts 复制代码
type Extract<T, U> = T extends U ? T : never

Omit

Omit<T, K>从类型T中剔除K中的所有属性。

ts 复制代码

Parameters

Parameters 获取函数的参数类型,将每个参数类型放在一个元组中。

ts 复制代码
type Parameters<T extends (...args: any) => any> = T extends (...args: infer U) => any ? U : never

ReturnType

ReturnType 获取函数的返回类型。

ts 复制代码
type ReturnType<T extends Function> = T extends (...args: any) => infer U ? U : never

Awaited

Awaited 获取 Promise 返回的类型

ts 复制代码
type Awaited<T extends Promise<any>> = T extends Promise<infer U> ? U : never

NonNullable

从类型中排除 null 和 undefined 来构造一个新的 Type

ts 复制代码
type NonNullable<T> = T extends null | undefined ? never : T

自定义类型工具

Prettier

在编辑器中,ts 总是优先展示接口名而不显示接口的实际内容:

这样我们就要时常要去接口的定义处看接口的实际内容到底是啥。为了能让接口直接显示出实际内容,可以自定义一个类型工具 Perttier :

ts 复制代码
type Prettier<T> = T extends infer U ? {[K in keyof U]: U[K]} : never

ValueOf

ValueOfkeyof相对应。取出类型的所有 value 的联合类型

ts 复制代码
type ValueOf<T extends Record<string, any>> = T[keyof T]

Mutable

用来将所有属性的readonly移除

ts 复制代码
type Mutable<T extends Record<string, any>> = {
  -readonly [P in keyof T]: T[P]
}

ConvertNumberToString

将类型中的number转换为string类型

ts 复制代码
type ConvertNumberToString<T extends Record<string, any>> = {
  [K in keyof T]: T[K] extends number ? string : T[K]
}

SymmetricDifference

SymmetricDifference<T, U>获取没有同时存在于T和U内的类型

ts 复制代码
type SymmetricDifference<T, U> = Exclude<T | U, T & U>

FunctionKeys

获取T中所有类型为函数的key组成的联合类型

ts 复制代码
type FunctionKeys<T extends Object> = {
  [K in keyof T]: <NonNullable<T[K]>> extends Function ? K : never
}[keyof T]

OptionalKeys

OptionalKeys<T>提取T中所有可选类型的key组成的联合类型

ts 复制代码
type OptionalKeys<T> = {
  [K in keyof T]: {} extends Pick<T, K> ? K : never
}[keyof T]
  • 核心实现,用映射类型遍历所有key,通过Pick<T, P>提取当前key和类型。注意,这里也是利用了同态拷贝会拷贝可选修饰符的特性。
  • 利用{} extends {当前key: 类型}判断是否是可选类型。

实战应用

使用泛型实现透传函数返回值

原先接口请求函数

ts 复制代码
const request = (params: { url: string; data: any; method: string }) => {
    return new Promise((resolve, reject) => {
        uni.request({
            ...params,
            async success(res: any) {
                if (res.data.code === 200) {
                    resolve(res.data)
                } else {
                    reject(res.data)
                }
            }
        })
    })
}
const getList = (): Promise<any> => {
    return request(
        {
            url: 'https://...',
            data: {},
            method: 'GET',
        }
    )
}
// 使用
const res = await getList() // res是any

因为 request 方法返回值类型是 Promise<unknown>, 所以只能给 getList 返回值定义 any 类型,导致使用时 ts 的类型推断为 any 失去了使用 ts 带来的便利。改造后:

ts 复制代码
interface IResoponse<T> {
    code: number
    msg: string
    data: T
}
const request = <T>(params: { url: string; data: any; method: string }): Promise<IResoponse<T>> => {
    return new Promise((resolve, reject) => {
        uni.request({
            ...params,
            async success(res: {data: IResoponse<T>}) {
                if (res.data.code === 200) {
                    resolve(res.data)
                } else {
                    reject(res.data)
                }
            }
        })
    })
}
const getList = () => {
    return request<string>(
        {
            url: 'https://...',
            data: {},
            method: 'GET',
        }
    )
}
// 使用
const res = await getList() // res是string

改造后 request 函数的返回值根据传入的泛型 T 确定:Promise<IResoponse<T>>。 现在可以将函数的返回值类型定义前置,实现每个接口都有不同的返回值。

根据函数返回值格式不同自定义函数类型返回值工具

业务中有这么个点,接口请求的apigetList,其返回值是 Promise<{ data: any; code: number }>格式的。在业务中使用时为了偷懒不单独引入返回值类型,可以这样做:

ts 复制代码
const list = ref<Awaited<ReturnType<typeof getList>>['data']>()

这样做没问题,但是看起来就很麻烦,毕竟那么多接口呢。所以进行封装:

ts 复制代码
type RealReturnType<T extends Function> =
    T extends (...args: any) => Promise<{ data: any; code: number }>
    ? Awaited<ReturnType<T>>['data']
    : T extends (...args: any) => Promise<any>
    ? Awaited<ReturnType<T>>
    : ReturnType<T>

首先限定入参 T 是函数类型,然后判断如果 T 继承了 (...args: any) => Promise<{ data: any; code: number }> 类型,则自动解构 Promise 并返回其 data。

其次如果不符合返回值格式为 { data: any; code: number } 的,则只解构 Promise 并返回。

最后如果不是 async 函数,则直接返回结果。可以看到主要是利用了 extends 关键字来判断函数类型,然后根据类型返回结果。

ts 复制代码
// 需要封装的函数(接口)
const fn1 = async () => new Promise(r => r({ data: ['接口返回内容']; code: 200 }))
// 解构返回值
const list = ref<RealReturnType<typeof fn1>>()


// 需要封装的函数(可能是些常量的工厂函数)
const fn1 = async () => new Promise(r => r({a: 'a', b: 'b'}))
// 解构返回值
const list = ref<RealReturnType<typeof fn1>>()
相关推荐
GIS程序媛—椰子24 分钟前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_00130 分钟前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端33 分钟前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x36 分钟前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
木舟100937 分钟前
ffmpeg重复回听音频流,时长叠加问题
前端
王大锤43911 小时前
golang通用后台管理系统07(后台与若依前端对接)
开发语言·前端·golang
我血条子呢1 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
黎金安1 小时前
前端第二次作业
前端·css·css3
啦啦右一1 小时前
前端 | MYTED单篇TED词汇学习功能优化
前端·学习