Skip to content

TS-05 高级类型

交叉类型

使用&符号定义交叉类型,也可以叫做与的操作

下面的例子组合两个对象,当组合两个对象时,就可以使用&的操作

ts
const mergeFunc = <T, U>(arg1: T, arg2: U): T & U => {
    let res = {} as T & U // 使用类型断言
    res = Object.assign(arg1, arg2)
    return res
}
mergeFunc({a: 'a'}, {b: 'b'})

联合类型

使用|符号定义联合类型

type1 | type2 | type3 即可以是type1也可以是type2或者type3

类型保护

ts
const valueList = [123, 'abc']
const getRandomValue = () => {
    const number = Math.random() * 10
    if(number < 5) return valueList[0]
    else return valueList[1]
}
const val = getRandomValue()
// 由于以上会返回两种类型,因此下面的判断会出错,需要使用类型断言
if((val as string).length) {
    console.log((val as string).length)
} else {
    console.log((val as number).toFixed())
}

上面的例子中,使用类型断言太多,比较麻烦,因此需要类型保护

  • 第一种类型保护: 使用类型谓词,谓词为parameterName is Type这种形式

    ts
    // 定义一个函数
    function isString(value: string | number): value is string {
        return typeof value === 'string'
    }
    // 之后再进行判断
    if(isString(item)){
        // 如果为true则该代码块的类型都是string类型的
        console.log(item.length)
    } else {
        console.log(item.toFixed())
    }
  • 第二种类型保护: typeof

    ts
    if(typeof item === 'string'){
        console.log(item.length)
    } else {
        console.log(item.toFixed())
    }

    typeof在ts中作为类型保护,使用需要注意以下几点:

    • 只能是全等或者全不等操作符,不能使用includes来,如:(typeof item).includes('string')是错误的
    • 只能是以下四种中的一种:stringnumberbooleansymbol
  • 第三种类型保护,instanceof 用与检测是否是某个类的实例

可以为null的类型

TS具有两种特殊类型:nullundefined,默认情况下,类型检查器会自动赋值null或者undefined,当开启--strictNullChecks标记时,当声明变量,不会再自动赋值null或者undefined,需要手动赋值了;

ts
let s = 'foo'
s = undefined // 报错

let sn: string | null = 'foot'
sn = null
sn = undefined // 报错, 没有undefined

注意: TypeScript会把nullundefined区别对待,string | nullstring | undefinedstring | null | undefined三种是不同的类型

类型断言

使用!来进行类型断言

使用!断言的情况为undefined的情况,声明一个值不赋初始值

ts
const getLength = (value: string | null): string {
    function getRes(prefix: string) {
        // 这里使用!来类型断言,因为num可能为undefined
        return prefix + num!.toFixed().toString()
    }
    num = num || 0.1
    return getRes(num)
}

类型别名

类型别名只是为类型起了一个名字,相当于js中的赋值

ts
type TypeString = string
let str: TypeString = 'str' // 可以直接使用类型别名了

对象别名只能在属性中引用自身,不能在其他地方使用自身

ts
// 嵌套例子, 在对象中引用自身
type Childs<T> {
	current: T
	child?: Childs<T>
}
// 错误引用例子
type Childs = Childs[]

注意: 当接口定义别名时,不能使用extendsimplements,因为别名是没有这两个属性的,只有接口有

当无法通过接口并且需要使用联合类型或者元组类型时,用类型别名,当需要使用extendsimplements时使用接口

字面量类型

  • 字符串字面量
ts
type Name = 'Heny'
let str: Name = 'Heny' // 只能是这一种类型

type Direction = 'north' | 'east' | 'south' | 'west'
function getDirectionFirstLetter(direction: Direction){
    return direction.substr(0, 1)
}
getDirectionFirstLetter('north') // 只能是四个中的其中一个,ts会自动提示有哪些,交互效果很好
  • 数字字面量和字符串字面量同理;

可辨识联合

单例类型:指的是枚举成员类型或者字面量类型

我们可以把单例类型、联合类型、类型保护、类型别名这几种类型进行合并,来创建可辨识联合类型的高级模式,也可以称作标签联合代数数据类型

三要素:

  1. 具有普通的单例类型属性
  2. 一个类型别名包含了哪些类型的联合
  3. 此属性上的类型保护
ts
interface Square{
    kind: 'square'
    size: number
}
interface Rectangle {
    kind: 'rectangle'
    height: number
    width: number
}
interface Circle {
    kind: 'circle'
    radius: number
}
type Shape = Square | Rectangle | Circle
function getArea(s: Shape) {
    switch(s.kind) {
        case 'square': return s.size * s.size
        case 'rectangle': return s.height * s.width
        case 'circle': return Math.PI * s.radius ** 2 // **表示平方
    }
}

上面的例子中,三个接口的kind就是一个单例类型属性,称为可辨识的属性或者标签,类型别名Shape包含了三个类型的联合,当走到switch代码里面时,会自动将可以访问的属性都列举出来

  • switch遗漏判断条件处理方式
ts
// 第一种,开启--strictNullChecks,并且指定getArea返回值类型

// 第二种定义一个错误函数
function asserNever(value: never): never => {
    throw new Error('Unexpected object: ' + value)
}
function getArea(s: Shape): number {
    switch(s.kind) {
        case 'square': return s.size * s.size
        case 'rectangle': return s.height * s.width
        case 'circle': return Math.PI * s.radius ** 2 // **表示平方
        default: return asserNever(s) // 如果前面再遗漏, 则会报错提示
    }
}

多态的this 类型

多态的this类型表示的是某个包含类或接口的子类型,它能很容易的表现连贯接口间的继承,比如在计算器的例子中,在每个操作之后都返回this类型:

ts
class Counter {
  constructor (public count: number = 0){}
  public add(value: number){
    this.count += value
    return this
  }
  public substract(value: number) {
    this.count -= value
    return this
  }
}
let counter: Counter = new Counter(5)
class PowCounter extends Counter {
  constructor(public count: number = 0) {
    super(count)
  }
  public pow(value: number) {
    this.count = this.count ** value
    return this
  }
}
let p: PowCounter = new PowCounter(5)
console.log(p.add(5).substract(2).pow(3))

索引类型

使用keyof连接符,连接一个类型,之后返回由这个类型的所有属性名组成的联合类型

ts
interface Info {
    name: string;
    age: number
}
let info: keyof Info; // info的类型将变成(name | age)类型;

函数使用keyof连接符:

ts
function getValue<T, K extends keyof T>(o: T, names: K[]): T[K][] {
	return names.map(n => o[n])    
}
const infoObj = {
    name: 'hh',
    age: 18
}
getValue(infoObj, ['name', 'age'])

索引访问类型

和访问对象的某个属性值是一样的,在ts中用来访问某个属性的类型,用在接口添加keyof时,会返回接口所有的类型组成的联合类型

ts
// 例子一
interface Info2 {
    name: string;
    age: number
}
type NameT = Info2['name'] // NameT为string类型

// 例子二
interface Type {
    a: never;
    b: string;
    c: number;
    d: undefined;
    e: null;
    f: object
}
type Test = Type[keyof Type] // keyof Type将是非never,undefined,null的其他类型组成的联合类型

使用[keyof T]访问的是接口类型中不为undefinednullnever类型组成的联合类型

映射类型

使用in连接符,in相当于for in遍历操作符;

当需要给一个接口里面的所有属性添加readonly修饰符时,可以使用in keyof操作符

ts
interface Info {
    name: string;
    age: number;
    size: number
}
type ReadInfo<T> = {
    // P相当于是T的每一个属性名,T就是接口,下面可以添加可选操作符或者readonly修饰符
    [P in keyof T]: T[P]
}
type ReadInfo1 = ReadInfo<Info>
// ReadInfo的类型即是Info里面的所有属性修改为只读的可选三个属性了;

TypeScript中,给接口的所有属性添加readonly修饰符和可选的修改符,一般是常用的,TypeScript提供了两个内置映射类型,可以直接拿来使用;

Readonly 只读映射 和 Partial可选映射,这两个都是经常使用的映射类型;

基本实现:

ts
type Readonly<T> = {
    readonly [P in keyof T]: T[P]
}
type Partial<T> = {
    [P in keyof T]?: T[P]
}

基本使用:

ts
type ReadInfo = Readonly<Info> // 使用前面定义的Info接口
type PartialInfo = Partial<Info>

最简单的映射类型和它的组成成分

ts
type Keys = 'option1' | 'option2'
type Flags = {[K in Keys]: boolean;}

它的语法与索引签名的语法类型,内部使用了for...in,具有三个部分:

  1. 类型变量K,它会依次绑定到每个属性。
  2. 字符串字面量联合的Keys,它包含了要迭代的属性名的集合
  3. 属性的结果类型

在个简单的例子里, Keys是硬编码的的属性名列表并且属性类型永远是 boolean,因此这个映射类型等同于:

ts
type Flags = {
    option1: boolean;
    option2: boolean;
}

在真正的应用里,可能不同于上面的 ReadonlyPartial。 它们会基于一些已存在的类型,且按照一定的方式转换字段。 这就是 keyof和索引访问类型要做的事情:

ts
type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }

但它更有用的地方是可以有一些通用版本

ts
type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P in keyof T]?: T[P] }

还有两个内置映射类型:PickRecord

ts
// Pick的实现,Pick指的是原来的对象里面的一部分属性名组成的一个类型
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}

// Record的实现 Record适用与将一个对象中的每一个属性转换为其他值的一个类型
type Record<K extends keyof any, T> = {
    [P in K]: T;
}

Pick的使用场景:

ts
interface Info {
    name: string;
    age: number;
    address: string;
}
const info: Info = {
    name: 'hh';
    age: 18;
    address: 'beijing'
}
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>{
    const res: any = {}
    keys.map(key => {
        res[key] = obj[key]
    })
    return res
}
const nameAddress = pick(info, ['name', 'address'])
// nameAddress的类型为:Pick<Info, 'name' | 'address'>

Record使用场景:常用于要把对象中的属性转换为其他值的一个场景

ts
function mapObject<K extends keyof string | number, T, U>(obj: Record<K, T>, f: (x: T) => U): Record<K, U>{
    let res: any = {}
    for(const key in obj) {
        res[key] = f(obj[key])
    }
    return res
}
const names = {0: 'hello', 1: 'wrod', 2: 'bye'}
const lengths = mapObject(names, s => s.length)

Record简单使用:

ts
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>

同态:两个相同类型的代数结构保持映射;

ReadonlyPartialPick都是同态的;Record不是同态,因为Record并不需要输入类型来拷贝属性,所以它不属于同态

非同态类型本质上会创建新的属性,因此它们不会从它处拷贝属性修饰符。

映射类型进行推断

  • 类型包装

T[P]被包装在Proxy<T>的类里

ts
type Proxy<T> = {
    get(): T,
    // set接收的值的类型必须和get返回值的类型相同
    set(val: T): void
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>
}
function proxify<T>(obj: T): Proxify<T>{
    const result = {} as Proxify<T>
    for(const key in obj){
        result[key] = {
            get: () => obj[key],
            set: (val) => obj[key] = val
        }
    }
    return result
}
let props = {
    name: 'heny',
    age: 18
}
let proxyProps = proxify(props)
  • 类型拆包
ts
function unproxify<T>(t: Proxify<T>): T{
    const result = {} as T
    for(const k in t) {
        result[k] = t[k].get()
    }
    return result
}
let originalProps = unproxify(proxyProps)

先看下怎么封装的,之后写个逆向的拆包,这个就叫做使用映射类型进行推断原始类型;

注意:这个拆包推断只适用于同态的映射类型。 如果映射类型不是同态的,那么需要给拆包函数一个明确的类型参数。

增加或移除特定修饰符

使用+为增加修饰符,使用-为移除特定修饰符

ts
type ReadonlyInfo<T> = {
    +readonly [P in keyof T]-?: T[P]
}

条件类型

ts2.8以后的类型,它会进行一个条件表达式,之后在其中的一个类型中选择一个类型,简单例子:T extends U ? X : Y

ts
type Type1<T> = T extends string ? string : nubmer

分布式条件类型

当待检测的类型是一个联合类型的时候,该条件类型就被称为分布式条件类型,在实例化的时候,ts会自动的分化成联合类型;

ts
type TypeName<T> = 
	T extends string ? string :
    T extends number ? number :
    T extends boolean ? boolean :
    T extends undefined ? undefined :
    T extends () => void ? () => void :
    object
    
type Type1 = TypeName<() => void> // 返回函数类型
type Type2 = TypeName<string[]> // 返回object类型
type Type3 = TypeName<(() => void) | string[]> // 返回联合类型

例子二:

ts
type Diff<T, U> = T extends U ? never : T
// 判断T的类型是否是U的子类型,经过判断,number是,因此只有number没有被返回回来
type Type2 = Diff<string | number | boolean, undefined | number>
// Type2的类型为 string | boolean

例子三:

ts
type Type3<T> = {
    [P in keyof T]: T[P] extends Function ? P : never
}[keyof T] // 索引访问类型,将所有的类型组成联合类型
interface Part {
    id: number;
    name: string;
    subParts: Part[];
    undatePart(name: string): void;
    subAdd(val: number): number;
}
type Type4 = Type3<Part>
// Types4的类型为 undatePart | subAdd,因为只有这两个是函数类型

条件类型的类型推断

使用infer关键字用来推断类型;

例子:当传入的是数组时,返回数组里面的元素的类型,如果传入的不是数组,则直接返回该类型

ts
// 不使用infer实现时,这里的T[number]为索引访问
type Type8<T> = T extends any[] ? T[number] : T
type Type9 = Type8<string[]> // 返回string类型,
type Type10 = Type8<string> //string

// 使用infer实现,利用一个新泛型变量,直接使用U和上面同理
type Type11<T> = T extends Array<infer U> ? U : T

预定义的有条件类型

  • Exclude<T, U> -- 从T中剔除可以赋值给U的类型。
  • Extract<T, U> -- 提取T中可以赋值给U的类型。
  • NonNullable<T> -- 从T中剔除nullundefined
  • ReturnType<T> -- 获取函数返回值类型。
  • InstanceType<T> -- 获取构造函数类型的实例类型。
ts
type Type1 = Exclude<'a' | 'b' | 'c', 'b' | 'c'> // Type1类型为'a'
type T03 = Extract<string | number | (() => void), Function>;  // () => void
type T10 = ReturnType<() => string>;  // string
type T11 = ReturnType<(s: string) => void>;  // void
function f1(s: string) {
    return { a: 1, b: s };
}
type T14 = ReturnType<typeof f1>;  // { a: number, b: string }

class C {
    x = 0;
    y = 0;
}
type T20 = InstanceType<typeof C>;  // C
type T21 = InstanceType<any>;  // any
type T22 = InstanceType<never>;  // any
type T23 = InstanceType<string>;  // Error
type T24 = InstanceType<Function>;  // Error

// 构造函数的默认参数写法
function fn(new (...args: any[]) => any) // 传入的就是一个构造函数

Released under the MIT License.