likes
comments
collection
share

TypeScrip从入门到进阶

作者站长头像
站长
· 阅读数 1

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

TS 诞生背景

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

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

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

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

TS 对 JS 的类型增强

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

foo.split(' '); // Uncaught TypeError: foo.split is not a function

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

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

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

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

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)的功能,提供代码补全、接口提示、跳转到定义、代码重构更牢靠等能力。

代码提示:

TypeScrip从入门到进阶

接口提示:

TypeScrip从入门到进阶

TS 对 JS 的语法增强

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

class:

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 语法。

装饰器:

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 数据类型

基本类型

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

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

字符串字面量类型

let str: 'small' | 'big' | 'fly'
str = 'small'
str = 'big'
str = 'fly'

任意类型

let a: any // 任意类型
a = 1
a = 'asdf'
a = {}

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

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

never 类型

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

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

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

unknown 类型

未知类型,类似 any ,没有 any 宽松

let a: unknown
let b = 2
a = b
b = a // 报错:unknown 不能赋值给 number 类型(any 可以)

类型断言

将类型断言为另一个类型

let a = {} as { a: string; b: number }
a = {
  a: 'asdf',
  b: 1,
}

联合类型

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

// 联合类型
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 // 报错

交叉类型

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

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

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

数组类型

数组类型有两种书写方式

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

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

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

元组

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

let a: [string, number] = ['a', 1]
a[0].length
a[1].toFixed()

函数

let fn = () => {} // 自动推导 () => void

let add2 = (a: number) => a + 2 // (a: number) => number

ts 的函数具有重载功能:

// 重载签名
function greet(person: string): string;
function greet(persons: string[]): string[];
  
// 实现签名
function greet(person: unknown): unknown { 
  if (typeof person === 'string') { 
    return `Hello, ${person}!`
  } else if (Array.isArray(person)) { 
    return person.map(name => `Hello, ${name}!`); 
  } 
  throw new Error('Unable to greet'); 
}

greet('World');          // 返回值类型:string
greet(['小智''大冶']);  // 返回值类型:string[]

greet(18);               // 报错:没有匹配的重载

对象的类型——接口

interface IResponse {
  code: number
  data: any
  msg: string
}
let res: IResponse = {
  code: 200,
  data: true,
  msg: '成功',
}

泛型

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

interface IResponse<T> {
  code: number
  msg: string
  data: T
}
let res: IResponse<boolean> = {
  code: 200,
  data: true,
  msg: '成功',
}

枚举

enum Status {
  SUCCESS = 200,
  FAIL = 0,
  NOTFOUND = 404,
}
if (code === Status.SUCCESS) {...}

TS 关键词特性

typeof 获取js变量的ts类型

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

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

let fn = () => {}
type Fn = typeof fn // 等同于 type Fn = () => void

keyof 索引查询和访问

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

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

in 属性遍历

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

type P<T> = {
  [K in keyof T]: T[K]
}

extends 继承条件判断

  • 用于接口继承
interface T1 {
  name: string,
}
interface T2 {
  sex: number,
}

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

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

// 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自己推到类型。

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之后、? 之前

is 类型谓词

is 用来判断一个变量属于某个接口或类型

// 判断参数是否为string类型, 返回布尔值
function isString(str: unknown): boolean {
  return typeof str === 'string'
}

// 判断参数是否为字符串,是在调用转大写方法
function ifUpperCase(str: unknown){

  if(isString(str)){
    str.toUpperCase()
    // (parameter) str: unknown
    // 报错:类型“unknown”上不存在属性“toUpperCase”
  }
}

示例中我们虽然判断了参数strstring类型, 但是条件为true时, 参数str的类型还是unknown.也就是说这个条件判断并没有更加明确str的具体类型。

// 判断参数是否为string类型, 返回布尔值
function isString(str:unknown): str is string{
  return typeof str === 'string'
}

// 判断参数是否为字符串,是在调用转大写方法
function ifUpperCase(str: unknown){

  if(isString(str)){
    str.toUpperCase() // (parameter) str: string
  }
}

str is string不仅返回boolean类型判断参数str是不是string类型, 同时明确的string类型返回到条件为true的代码块中。

因此当我们判断条件为true, 即strstring类型时, 代码块中str类型也转为更明确的string类型。

类型兼容性

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

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

可赋值性

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

在接口中:

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

let animal: Animal;
let dog: Dog;

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

在联合类型中:

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

b = a; // 不可赋值
a = b; // 可以赋值

协变

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

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>,这就是协变的。

逆变

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

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推导的名称相同并且都处于逆变的位置,则推导的结果将会是交叉类型

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推导的名称相同并且都处于协变的位置,则推导的结果将会是联合类型

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的所有属性变成可选的。

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

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

type PartialOption<T, U extends keyof T> = Omit<T, U> & Partial<Pick<T, U>>

Required

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

type Required<T> = {
  [P in keyof T]-?: T[P]
}

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

Readonly

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

type Readonly<T> = {
  readonly [K in keyof T]: T[K]
}

Pick

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

type Pick<T, P extends keyof T> = {
  [K in P]: T[K]
}

Record

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

type Record2<U extends keyof any, T> = {
  [K in U]: T
}

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

Exclude

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

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

Extract

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

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

Omit

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

type Omit<T, K extends keyof any> = {
  [P in keyof T as P extends K ? never : P]: T[P]
}
  • P in keyof T 是典型的类型映射语法,遍历类型 T 的所有属性并赋值给 P
  • P extends K ? never : P 是条件类型语法,如果 P 继承了 K 则返回 never,否则返回 P
  • 若条件语法结果是 never,则属性为 P in keyof T as nevernever 属性会被自动忽略。
  • 因此断句其实是 P in keyof T as (P extends K ? never : P)

下图为 as never 被忽略的证明: TypeScrip从入门到进阶

Parameters

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

type Parameters<T extends (...args: any) => any> = T extends (...args: infer U) => any ? U : never

ReturnType

ReturnType 获取函数的返回类型。

type ReturnType<T extends Function> = T extends (...args: any) => infer U ? U : never

Awaited

Awaited 获取 Promise 返回的类型

type Awaited<T extends Promise<any>> = T extends Promise<infer U> ? U : never

NonNullable

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

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

自定义类型工具

Prettier

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

TypeScrip从入门到进阶

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

type Prettier<T> = T extends infer U ? {[K in keyof U]: U[K]} : never

TypeScrip从入门到进阶

ValueOf

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

type ValueOf<T extends Record<string, any>> = T[keyof T]

Mutable

用来将所有属性的readonly移除

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

ConvertNumberToString

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

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

SymmetricDifference

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

type SymmetricDifference<T, U> = Exclude<T | U, T & U>

FunctionKeys

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

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

OptionalKeys

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

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

实战应用

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

原先接口请求函数

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 带来的便利。改造后:

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 }>格式的。在业务中使用时为了偷懒不单独引入返回值类型,可以这样做:

const list = ref<Awaited<ReturnType<typeof getList>>['data']>()

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

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 关键字来判断函数类型,然后根据类型返回结果。

// 需要封装的函数(接口)
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>>()

通过泛型和类型谓词解决联合类型非交叉属性使用问题

interface interfaceA {
  name: string;
  age?: number;
}
interface interfaceB {
  name: string;
  phone?: number;
}

const arr1: interfaceA[] = [{ name: "andy", age: 2 }];
const arr2: interfaceB[] = [{ name: "andy", phone: 2 }];
const target = [...arr1, ...arr2][0] // target: interfaceA | interfaceB

target.age // 报错: 类型“interfaceB”上不存在属性“age”
target.phone // 报错: 类型“interfaceA”上不存在属性“phone”

// 解决方案1
if ((target as interfaceA).age) {
  ;(target as interfaceA).age = 18
}

// 解决方案2
if ('age' in target) {
  // 此时 target 被推断为 interfaceA
  target.age = 18
} else {
  // 此时 target 被推断为 interfaceB
  target.phone = 10086
}

上述解决方案中,使用断言无法实现一次判断在一个代码块内生效,而 in 运算符可以。

现在我们还有新的方案 is

// 通过泛型定义通用类型保护函数
function isOfType<T>(target: unknown, prop: keyof T): target is T {
  return (target as T)[prop] !== undefined;
}

// 类型保护
if (isOfType<interfaceA>(target, "age")) {
  console.log(target.age);
} 
if (isOfType<interfaceB>(target, "phone")) {
  console.log(target.phone);
}

利用泛型实现不同按钮类型封装

type CustomType  = {
  radio: {
    values: {label: string; value: string;}[];
  },
  select: {
    options: {key: string; value: string}[];
  },
};

// form item类型 如果有自有属性的就在上面声明,没有就直接在前面申明
type Type = 'input'|'number-input'| keyof CustomType;

// 公共类型
interface CommonType<T extends Type> {
  id: string;
  key: string;
  label: string;
  type: T;
}

// 实际类型
type RealType<T extends Type> = T extends keyof CustomType ? CustomType[T] & CommonType<T> : CommonType<T>;

// Radio的数据类型
type RadioType = RealType<'radio'>;
// select的数据类型
type SelectType = RealType<'select'>;

const test: RadioType = {
  id: '',
  key: '',
  label: '',
  type: 'radio',  // 这里如果不写radio ts校验会报错  radio1  assignable radio
  values: [
    {  // 这里如果乱声明也会报错
      label: '',
      value: '',
    },
  ], // 如果values不写或者不是对应的数组类型 ts校验也会报错  缺少values
};

const test1: SelectType = {
  id: '',
  key: '',
  label: '',
  type: 'select', // 这里如果不写select ts校验会报错  'radio'  assignable 'select'
  options: [
    {
      key: '',
      value: '',
    },
  ], // 如果options不写或者不是对应的数组类型 ts校验也会报错  缺少option
};