TypeScript核心进阶知识点 [高级类型、泛型]

364 阅读10分钟

高级类型(不涉及到泛型)

联合类型

一个变量可以支持多种类型,极大提高了类型的可扩展性

let num: number | string

num = 1
num = 'one'

使用变量方法时,只能使用几种属性共有的方法

image.png

如果使用了共有属性以外的方法,就会报错,例如:直接访问 length 属性,string 类型上有,number 类型上没有,就报错了。

image.png

交叉类型

要对对象形状进行扩展,可以使用交叉类型 &

interface Person {
    name: string
    age: number
}

type Student = Person & { grade: number }

const student: Student = {
    name: 'Alice',
    age: 18,
    grade: 12
};
console.log(student)

结果:

image.png

类型保护

function getLength(arg: number | string): number {
    return arg.length
}

因为 number 类型上没有 length 属性,所以要报错

image.png

解决方法:使用 typeof 关键字判断变量的类型,最后都得到 number 类型的值

function getLength(arg:number | string): number {
    if(typeof arg === 'number'){
        return arg
    }else{
        return arg.toString().length
    }
}

字面量类型

type Sex = '男' | '女'

定义好了字面量类型值,不能赋定义以外的值

image.png

泛型

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

以现在知道的知识,有一些问题是很难解决的,例如下面的例子:

定义一个 print 函数,这个函数的功能是把传入的参数打印出来,再返回这个参数,传入参数的类型是 string,函数返回类型为 string。

function print(arg:string):string {
    console.log(arg)
    return arg
}

现在需求变了,我还需要打印 number 类型,怎么办?

可以使用联合类型来改造:

function print(arg:string | number):string | number {
    console.log(arg)
    return arg
}

现在需求又变了,我还需要打印 string 数组、number 数组,甚至任何类型,怎么办?

有个笨方法,支持多少类型就写多少联合类型。

或者把参数类型改成 any。

function print(arg:any):any {
    console.log(arg)
    return arg
}

const res:string = print(123)

且不说写 any 类型不好,毕竟在 TS 中尽量不要写 any,而且这也不是我们想要的结果,只能说传入的值是 any 类型,输出的值是 any 类型,传入和返回并不是统一的

泛型就可以轻松解决输入输出要一致的问题,但是泛型不止能解决这个问题,还有很多问题都可以用泛型来解决

image.png

泛型变量

也就是说使用大写字母 A-Z 定义的类型变量都属于泛型,把 T 换成 A,也是一样的

常见的泛型变量代表

  • T(Type):表示一个 TypeScript 类型
  • K(Key):表示对象中的键类型
  • V(Value):表示对象中的值类型
  • E(Element):表示元素类型

泛型基本使用

泛型的语法是 <> 里写类型参数,一般可以用 T 来表示

解决上面的问题

function print<T>(arg:T):T {
    console.log(arg)
    return arg
}
const res:number = print(123)

这样,就实现了输入和输出的类型统一,且可以输入输出任何类型。

泛型中的 T 就像一个占位符、或者说一个变量,在使用的时候可以把定义的类型像参数一样传入,它可以原封不动地输出

使用的时候可以有两种方式指定类型:

  • 定义要使用的类型
  • TS 类型推断,自动推导出类型
// 定义要使用的类型
function print<T>(arg:T):T {
    console.log(arg)
    return arg
}
const res = print<number>(123)

// TS 类型推断,自动推导出类型
function print<T>(arg:T):T {
    console.log(arg)
    return arg
}
const res = print(123)
console.log(typeof res === 'number') // true

自动推导类型,结果:

image.png

如果变量类型用到接口

interface Iprint<T> {
    (arg: T): T
}
function print<T>(arg: T): T {
    console.log(arg)
    return arg
}
const myPrint: Iprint<number> = print<number>

const bb = myPrint(123)
console.log(typeof bb === 'number')  // true

默认参数

// 默认为 number 类型
interface Iprint<T = number> {
    (arg: T): T
}

function print<T>(arg:T) {
    console.log(arg)
    return arg
}

const myPrint: Iprint = print

const bb = myPrint(123)
console.log(typeof bb === 'number') // true

处理多个函数参数

现在函数接收到一个元组,元组中有两项数据,交换元组的两项数据,并返回该元组

function swap(tuple) {
    return [tuple[1], tuple[0]]
}

如果这样写的话,没有定义元素类型,默认用 any 类型

image.png

用泛型进行解决问题

function swap<T, U>(tuple: [T, U]): [U, T]{
    return [tuple[1], tuple[0]]
}

const a = swap(['lll',123])

传入的参数里,第 0 项为 string 类型,第 1 项为 number 类型。

在返回值里,第 0 项为 number 类型,第 1 项为 string 类型。

image.png

第 0 项上全是 number 的方法。

image.png

第 1 项上全是 string 的方法。

image.png

约束泛型

现在有一个函数,功能是打印传入参数的长度

function printLength<T>(arg: T): T {
    console.log(arg.length)
    return arg
}

不确定 T 是否有 length 属性,肯定会报错的

image.png

这里可以和 interface 结合,来约束类型。

interface ILength {
    length: number
}

function printLength<T extends ILength>(arg: T): T {
    console.log(arg.length)
    return arg
}

关键就是 <T extends ILength>,让这个泛型继承接口 ILength

传入的参数,必须有 length 属性,没有就会报错

image.png

泛型约束类

定义一个栈,有入栈和出栈两个方法,如果想入栈和出栈的元素类型统一,就可以这么写:

class Stack<T> {
    private data: T[] = []
    push(item:T) {
        return this.data.push(item)
    }
    pop():T | undefined {
        return this.data.pop()
    }
}

在定义实例的时候写类型,比如,入栈和出栈都要是 number 类型,就这么写:

const s1 = new Stack<number>()

如果入栈的是一个字符串,直接报错

image.png

如果需求变了,想要入栈一个字符串类型的值

const s1 = new Stack<string>()

注意:泛型无法约束类的静态成员

例如:给pop 方法定义 static 关键字,将 pop 方法变成一个静态方法,就报错了

image.png

泛型约束接口

使用泛型,也可以对 interface 进行改造,让 interface 更灵活。

interface IKeyValue<T, U> {
    key: T
    value: U
}

const k1:IKeyValue<number, string> = { key: 18, value: 'lin'}
const k2:IKeyValue<string, number> = { key: 'lin', value: 18}

泛型定义数组

不用泛型

const arr: number[] = [1,2,3]

用泛型

const arr: Array<number> = [1,2,3]

高级类型(二)

索引类型

从对象中抽取一些属性的值,然后拼接成数组

const userInfo = {
  name: 'lin',
  age: '18',
}

function getValues(userInfo: any, keys: string[]) {
  return keys.map(key => userInfo[key])
}

// 抽取指定属性的值
console.log(getValues(userInfo, ['name','age']))  // ['lin', '18']
// 抽取obj中没有的属性:
console.log(getValues(userInfo, ['sex','outlook']))  // [undefined, undefined]

虽然 obj 中并不包含 sex 和 outlook 属性,但 TS 编译器并未报错

映射类型

TS允许将一个类型映射成另外一个类型。

为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等,下面出现了很多工具类型

in

介绍映射类型之前,先介绍一下 in 操作符,用来对联合类型实现遍历。

type Person = "name" | "school" | "major"

type Obj =  {
  [p in Person]: string
}

image.png

Partial

Partial<T>T的所有属性映射为可选的,例如:

interface IPerson {
    name: string
    age: number
}

let p1: IPerson = {
    name: 'lin',
    age: 18
}

使用了 IPerson 接口,就一定要传 name 和 age 属性,

image.png

使用 Partial 改造一下,就可以变成可选属性,

interface IPerson {
    name: string
    age: number
}

type IPartial = Partial<IPerson>

let p1: IPartial = {}

Partial 原理

Partial 的实现用到了 inkeyof

/**
 * Make all properties in T optional
 */
type Partial<T> = {
    [P in keyof T]?: T[P]
}
  • [P in keyof T]遍历T上的所有属性
  • ?:设置属性为可选的
  • T[P]设置类型为原来的类型

Readonly

Readonly<T>T的所有属性映射为只读的,例如:

interface IPerson {
  name: string
  age: number
}

type IReadOnly = Readonly<IPerson>

let p1: IReadOnly = {
  name: 'lin',
  age: 18
}

image.png

Readonly 原理

Partial 几乎完全一样,

/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
    readonly [P in keyof T]: T[P]
}
  • [P in keyof T]遍历T上的所有属性
  • readonly设置属性为只读的
  • T[P]设置类型为原来的类型

Pick

Pick用于抽取对象子集,挑选一组属性并组成一个新的类型,例如:

interface IPerson {
  name: string
  age: number
  sex: string
}

type IPick = Pick<IPerson, 'name' | 'age'>


let p1: IPick = {
  name: 'lin',
  age: 18
}

这样就把 name 和 age 从 IPerson 中抽取出来。

image.png

Pick 原理

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P]
}

Pick映射类型有两个参数:

  • 第一个参数T,表示要抽取的目标对象
  • 第二个参数K,具有一个约束:K一定要来自T所有属性字面量的联合类型

Record

上面三种映射类型官方称为同态,意思是只作用于 obj 属性而不会引入新的属性。

Record 是会创建新属性的非同态映射类型。

interface IPerson {
  name: string
  age: number
}

type IRecord = Record<string, IPerson>

let personMap: IRecord = {
   person1: {
       name: 'lin',
       age: 18
   },
   person2: {
       name: 'liu',
       age: 25
   } 
}

image.png

Record 原理

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
    [P in K]: T
}

Record 映射类型有两个参数:

  • 第一个参数可以传入继承于 any 的任何值
  • 第二个参数,作为新创建对象的值,被传入。

条件类型

T extends U ? X : Y 
//若类型 T 可被赋值给类型 U,那么结果类型就是 X 类型,否则就是 Y 类型

Exclude 和 Extract 的实现就用到了条件类型。

Exclude

Exclude 意思是不包含,Exclude<T, U> 会返回 联合类型 T 中不包含 联合类型 U 的部分。

type Test = Exclude<'a' | 'b' | 'c', 'a'>

image.png

Exclude 原理

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T
  • never表示一个不存在的类型
  • never与其他类型的联合后,为其他类型
type Test = string | number | never  

image.png

Extract

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

type Test = Extract<'key1' | 'key2', 'key1'>

image.png

Extract 原理

/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never

工具类型

索引类型、映射类型和条件类型都是工具类型。

下面在做一个补充

Omit

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

interface IPerson {
    name: string
    age: number
}

type IOmit = Omit<IPerson, 'age'>

这样就剔除了 IPerson 上的 age 属性。

image.png

Omit 原理

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = 
Pick<T, Exclude<keyof T, K>>

Pick用于挑选一组属性并组成一个新的类型,Omit 是剔除一些属性,留下剩余的,他们俩有点相反的感觉。

那么就可以用 Pick 和 Exclude 实现 Omit。

当然也可以不用 Pick 实现,

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

NonNullable

NonNullable<T> 用来过滤类型中的 null 及 undefined 类型。

type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]

NonNullable 原理

/**
 * Exclude null and undefined from T
 */
type NonNullable<T> = 
T extends null | undefined ? never : T
  • never表示一个不存在的类型
  • never与其他类型的联合后,为其他类型

Parameters

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

type T1 = Parameters<() => string>  // []

type T2 = Parameters<(arg: string) => void>  // [string]

type T3 = Parameters<(arg1: string, arg2: number) => void> // [arg1: string, arg2: number]

Parameters 原理

/**
 * Obtain the parameters of a function type in a tuple
 */
type Parameters<T extends (...args: any) => any> = 
T extends (...args: infer P) => any ? P : never

在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。

  • Parameters首先约束参数T必须是个函数类型
  • 判断T是否是函数类型,如果是则使用infer P暂时存一下函数的参数类型,后面的语句直接用 P 即可得到这个类型并返回,否则就返回never

ReturnType

ReturnType 获取函数的返回值类型。

type T0 = ReturnType<() => string>  // string

type T1 = ReturnType<(s: string) => void>  // void

ReturnType 原理

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = 
T extends (...args: any) => infer R ? R : any

懂了 Parameters,也就懂了 ReturnType,

  • ReturnType首先约束参数T必须是个函数类型
  • 判断T是否是函数类型,如果是则使用infer R暂时存一下函数的返回值类型,后面的语句直接用 R 即可得到这个类型并返回,否则就返回any

其他基础知识点

Ts基础知识点(一)[ Ts与Js区别、Ts基础类型、Ts断言 ]

Ts基础知识点(二)[函数、类、接口、接口约束类]

Ts基础知识点(三)[ 类型推论、枚举、类型守卫、联合类型与交叉类型、Ts 装饰器、Ts 配置文件]