Skip to content

Typescript中的高级类型

作者:guo-zi-xin
更新于:9 个月前
字数统计:3.1k 字
阅读时长:11 分钟
Typescript高级类型

交叉类型(Intersection Types): &

交叉类型是将多个类型合并为同一个类型,这让我们可以把现有的类型叠加到一起成为一种类型, 它包含了所需的所有类型的特性。

👋 不适用于基本类型 如string类型、number类型

关于交叉类型产生never类型

交叉类型在使用的时候有时候会产生一个新的类型never, 一般产生这种情况是两个interface使用交叉类型进行处理, 它们当中都有一个name的同名属性,但两个类型不同,interface1中的namestring类型, interface2name属性是number类型, 但没有一个属性的类型即是string类型又是number类型

typescript
interface IDefaultFirst {
  name: string;
}
interface IDefaultSecond {
  name: number;
}

type CrossType = IDefaultFirst & IDefaultSecond; // CrossType中的name的类型将为‘never’
  • 示例

    具体类型联合

    typescript
    interface First {
      name: string;
    }
    
    interface Second {
      question: sstring;
      id: numbher;
    }
    
    const getCrossover = <First, Second>(first: First, second: Second): First & Second => {
      // Partial 是Typescript的工具类型  
      // Partial<Type>
      // 作用是 构造类型 Type ,并将它所有的属性设置为可选的。它的返回类型表示输入类型的所有子类型
      const result: Partial<First & Second> = {}
      for (const key in first) {
        if (first.hasOwnProperty(key)) {
          (result as First)[key] = first[key]
        }
      }
      for (const keys in second) {
        if (second.hasOwnPropertu(keys)) {
          (result as Second)[keys] = seconds[keys]
        }
      }
      return result as First & Second // 这个类型断言可以省略
    }
    const obj = getCrossover({ a: 'hello' }, { b: 42 })
    
    // 现在 obj就同时拥有了 a 属性与 b 属性

    泛型联合

    typescript
    const getCrossover = <T extends object, U extends object>(first: T, second: U): T & U => {
      const result = <T & U>{}
      for (const key in first) {
        (<T>result)[key] = first[key]
      }
      for (const keys in second) {
        if (!result.hasOwnProperty(keys)) {
          (<U>result)[keys] = second[keys]
        }
      }
      return result
    }
    
    const obj = getCrossover({ a: 'hello' }, { b: 42 })
    
    // 现在 obj就同时拥有了 a 属性与 b 属性

    使用交叉类型进行接口混入

    typescript
    // 初始时的 question 和 answer 定义
    
    interface IQuestionRecord {
      createTime: string;
      userName: string;
      userAvatar: string;
      question: {
        title: string;
        content: string;
        picture: string[];
      };
    }
    
    interface IAnswerRecord {
      createTime: string;
      userName: string;
      userAvatar: string;
      answer: {
        comment: string;
        audio?: {
          url: string;
        };
      };
    }
    
    
    // 👇👇👇👇👇通过提取公共部分, 利用联合类型将类型定义简化并且可复用
    interface IUserBaseinfo {
      createTime: string;
      userName: string;
      useAvatar:string;
    }
    
    interface IQuestionRecord {
      question: {
        title: string;
        content: string;
        picture: string[]
      }
    }
    interface IAnswerRecord {
      answer: {
        comment: string;
        audio?: {
          url: string
        }
      }
    }
    
    // 使用交叉类型混入
    
    // Mixin 类型的含义是遍历传入的泛型 T 和 U 的所有属性, 并且把它们联合,产生新的类型对象, 新的类型对象上每个属性的类型是它们在原本类型上的属性
    // Mixin<T,U>  是泛型类的写法
    type Mixin<T,U> = {
      [P in keyof (T & U)]: (T & U)[P]
    }
    // 另一种写法
    type Mixin<T, U> = T & U
    
    // 使用泛型混入, 方便之后复用
    // MixinUserInfo 类型的含义是通过调用混合类型 Mixin, 先将共同的部分IUserBaseInfo类型先混入进去, 整合成新类型, 方便给具体的 question 和 answer 来调用
    type MixinUserInfo<T> = Mixin<IUserBaseInfo, T>
    
    // 这里的 IRecordConfig 是把 question 和 answer 整合成一个类型, 减少不必要的类型定义
    interface IRecordConfig {
      question?:MixinUserInfo<IQuestionRecord>
      answer?: MixinUserInfo<IAnswerRecord>
    }
    // 最终使用的时候的类型
    export type RecordConfigList = IRecordConfig[]

联合类型(Union Types): |

联合类型与交叉类型很有关联,但是使用上完全不同。 联合类型会产生一个包含所有类型的选择集的类型,表示一个值的类型是定义的联合类型中的其中一种。

当一个变量希望传入某种类型时,可以考虑使用联合类型

当一个值是联合类型对象时, 我们只能访问这个联合类型中的所有类型中的共同成员

  • 示例

定义常量

typescript
type IQuestionType = string | number | boolean

// 这里表示id的类型是布尔值 它也可以定义为字符串 数字 都是可以正常运行的
const id:IQuestionType = false

在函数中定义

typescript
const getUnoinType = (value: string, padding: string | number) => {
  if (typeof padding === 'number') {
    return Array(padding + 1).join('') + value
  } else if (typeof padding === 'string') {
    return padding + value
  }
  throw new Error(`Expected string or number, got '${padding}'`)
}
getUnoinType("Hello world", 4);

当一个值是联合类型对象时, 我们只能访问这个联合类型中的所有类型中的共同成员

typescript
  interface Bird {
    fly(): void;
    layEggs(): void;
  }

  interface Fish {
    swim(): void;
    layEggs(): void;
  }

const getSmallPet = ():Bird | Fish => {
  // ...
}
const pet = getSmallPet(); 
pet.layEggs(); // 正常运行
pet.swim(); // 报错, Bird 类型中没有这个属性

类型守卫(Type Guards)

类型守卫是一种用于收窄或者断言变量的技术, 通常与联合类型与交叉类型一起使用。

类型守卫可以通过一些条件检查来确定变量的确切类型, 以便在后续的代码中使用更具体的类型信息

类型守卫通常有以下几种方式

typeof类型守卫

使用typeof操作符检查变量类型

typescript
const printValue = (value: string | number) => {
  // 这里使用 typeof 操作符将 value 值的类型范围收窄到 string 类型, 之后就可以调用字符串的方法
  if (typeof value === 'string') {
    console.log(value.toUpperCase())
  // else判断体里的逻辑是将 value 的类型推断为 number类型, 之后调用 Number 类型的方法
  } else {
    console.log(value.toFixed(0))
  }
}

instanceof类型守卫

使用instanceof操作符检查对象是否属于某个类

typescript
class Cat {
  meow() {
    console.log('Meow')
  }
}

class Dog {
  bark() {
    console.log('Bark')
  }
}

const makeSound = (animial: Cat | Dog) => {
  // 这里使用 instanceof 操作符将 animial 的类型收窄到 Cat 类上, 之后调用 meow() 方法
  if (animial instanceof Cat) {
    animial.meow()
  // else 判断体的逻辑是将 animial 推断为属于 Dog 这个类型, 之后调用bark方法
  } else {
    animial.bark()
  }
}

自定义类型守卫

通过定义一个返回类型谓词的函数, 来自定义一个类型守卫

🫸 类型谓词 的形式是 paramterName is Type这种形式, paramterName必须是来自当前函数签名里的一个参数名, Type表示一个类型 🫷

typescript
interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

// pet is Fish 这一段就是类型谓词
const isFish = (pet: Bird | Fish):pet is Fish => {
  return (pet as Fish).swim !== undefined
}

const getSmallPet = ():Bird | Fish => {
  // ...
}
const pets = getSmallPet(); 
// 这里 通过自定义的 类型谓词 将pets的类型收窄为 Fish, 之后调用 Fish 类型定义的操作函数
if (isFish(pets)) {
  pets.swim()
// else判断体中是将 pets 类型推断为 Bird 类型, 之后调用 Bird 类型定义的操作函数
} else {
  pet.fly()
}

in操作符

in操作符可以作为类型细化表达式来使用

对于n in x表达式, 其中n是字符串字面量或字符串字面量类型, 并且x是个联合类型, 那么true分支的类型细化为有一个可选的或者必须的属性, false分支的类型细化为有一个可选的或不存在属性n

typescript
const move = (pet: Fish | Bird) => {
  if ('swim' in pet) {
    return pet.swom();
  }
  return pet.fly();
}

类型别名

类型别名, 顾名思义, 就是给一个类型起一个新名字, 但是不会新创建一个类型。

类型别名有时候和接口很相似, 但是可以作用于原始值、联合类型,元组预计其它任何需要手写的类型。

但是不需要给原始类型起别名,通常没什么用处, 尽管可以运行。

typescript
type Name = string

type NameResolver = () => string

type NameOrResolver = Name | NameResolver

const getUserName = (name: NameOrResolver): Name => {
  if (typeof name === 'string') {
    return name;
  } else {
    return name();
  }
}

泛型式类型别名

同接口一样, 类型别名可以是泛型 - 我们可以添加参数类型并且在别名声明的右侧传入:

typescript
type Container<T> = { value: T};

// 在类型别名属性中引用自身

type Tree<T> = {
  value: T;
  left: Tree<T>;
  right: Tree<T>;
}

与交叉类型一起使用:

typescript
type LinkedList<T> = T & { next: linkedList<T> };

interface Person {
  name: string;
}

const people: LinkedList<Person>

const name1 = people.name;
const name2 = people.next.name;
const name3 = people.next.next.name;
const name4 = people.next.next.next.name ;

TIP 🔔

  • 类型别名不能出现在右侧任何地方。

    typescript
    type Yikes = Array<Yikes> // 这个写法会报错
  • 如果需要使用类型注解的层次结构,请使用接口。 它能使用implementsextedns

  • 为一个简单的对象类型使用类型别名, 只需要一个与异化的名字就可以。 另外。 当想给联合类型和交叉类型提供一个语义化的名称时, 类型别名是一个好的选择。

  • 请注意,类型别名在 TypeScript 中只是给现有类型起了一个别名,它们并不会创建出不同或独立的类型。当你使用类型别名时,实际上就相当于直接使用了被别名的原始类型。换句话说,类型别名并不会创建出全新的、不同的类型。

typescript
type A = { x: number };
type B = A;

let a: A = { x: 42 };
let b: B = { x: 10 };

a = b;  // 合法
b = a;  // 合法

上述示例中,类型别名 B 被定义为类型 A 的别名,因此变量 a 和 b 可以互相赋值,因为它们实际上都是指向相同的类型。尽管在代码中看起来好像创建了两个不同的类型,但在 TypeScript 视角下,它们实际上是完全相同的类型

接口与类型别名

类型别名虽然可以和接口一样声明, 但是它们并不同。

  1. 接口创建了一个新的名字,可以在其它任何地方使用,但类型别名并不创建新名字 ——例如, 错误信息就不会使用别名。在下面示例中,在编辑器中将鼠标悬停在interfaceed上, 显示它返回的是Interface,但悬停在aliased上时,现实的却是这个字面量类型:

    typescript
    type Alias = { num: number };
    
    interface interface {
     num: number
    };
    
    declare const aliased = (arg: Alias):Alias => {}
    declare const interfaced = (arg: Interface): Interface => {}

    在旧版本的 TypeScript 里,类型别名不能被继承和实现(它们也不能继承和实现其它类型)。从 TypeScript 2.7 开始, 类型别名可以被继承并生成新的交叉类型。例如: type Cat = Animal & { purrs: true } 。

    因为软件中的对象应该对于扩展是开放的,但是对于修改是封闭的 (opens new window),你应该尽量去使用接口代替类型别名。

  2. 如果无法通过接口来描述一个类型并且需要使用联合类型或元组类型, 这个时候通常会使用类型别名

元组类型(Tuple)

用于表示固定长度和固定类型排列的数组。在元组中,每个位置上的元素都有一个确定的类型

typescript
let x: [string, number]
// 初始化
x = ['hello', 10]
// 错误的初始化
x = [10, 'hello']

但是访问一个已知的索引, 会得到正确的类型

typescript
console.log(x[0].substr(1)) // ok
console.log(x[1].substr(1)) // Error number 类型没有substr方法

元组类型在需要固定长度和类型的数组场景下非常有用,例如表示一对坐标、表示函数返回多个不同类型的值等。通过使用元组类型,可以更好地约束数组的结构,提高代码的类型安全性。

当访问超出已知索引的元素时,会返回元组包含的类型的所有联合类型

infer 关键字

表示在extends条件语句中待推断的类型变量,它是从泛型里面进行推断

typescript
type ParamType<T> = T extends (arg: infer P) => any ? P: T

在这个条件语句中 T extends (arg: infer P) => any ? P : T 中, infer P表示待推断的函数参数

整句的含义为: 如果T 能赋值给 (arg: infer P) => any, 则结果是 (arg: infer P) => any 类型中的参数P否则返回T

typescript
  type shiftArr<arr extends unknown[]> = arr extends [unknown, ...infer restArr]
    ? restArr
    : never;

  type footArr = shiftArr<[1, 2, 3]>;

上面这个示例得到的结果是得到一个去掉首位字符的数组[2,3], 但它不是结果, 它是一个类型

整句的含义为shiftArr类型中传入了泛型arrarr是继承于(或者说arr的类型范围限制在了)unknown数组, 我们通过数组解构的语法 将除去首位字符的元素推断成一个新类型restArr,如果这个restArr存在,那么就返回这个新类型,否则就返回never类型

引用

深入理解Typescript
https://jkchao.github.io/typescript-book-chinese/typings/overview.html#%E7%B1%BB%E5%9E%8B%E5%88%AB%E5%90%8D
TypeScript手册
https://bosens-china.github.io/Typescript-manual/download/zh/reference/advanced-types.html#%E7%B1%BB%E5%9E%8B%E5%AE%88%E5%8D%AB%E4%B8%8E%E7%B1%BB%E5%9E%8B%E5%8C%BA%E5%88%86-type-guards-and-differentiating-types
这才是真正让你入门Typescript类型体操的文章
https://juejin.cn/post/7283797053338517545?searchId=202312121416195FE61D891B64900A0F78#heading-7

人生没有捷径,就像到二仙桥必须要走成华大道。