Typescript 常用写法-类型联动

820 阅读5分钟

什么是类型联动

类型联动是指在一个对象中,当某个属性的值发生变化时,其他属性的类型也随之变化。这种机制使得类型系统更加灵活和强大,能够更好地描述复杂的数据结构。

假设有这样一个类型:

type Component = {
  type: 'setUserName' | 'setUserList' | 'steUserInfo';
  data: stringstring[]|{ test: string };
}

类型联动,指的是当 type 的值发生变化时,data的类型也跟着变化,比如当type为setUserName时,可能希望data为string,而当type为setUserList时,则希望data为string[]

编码实战

不同type不同Data数据类型

期望: 当 type = 'setUserName' 时,data的类型是 stringtype = 'setUserList' 时, data 的类型是 string[]type = 'steUserInfo' 时, data 的类型是 { test: string }

type DataObj = {
  setUserName: string;
  setUserList: string[];
  steUserInfo: { name: string };
};

type Data = {
  [T in keyof DataObj]: {
    type: T;
    data: DataObj[T];
  };
}[keyof DataObj];

const data1: Data = {
  type: 'user',
  data: 'tds',
};

const data2: Data = {
  type: 'setUserName',
  data: ['tds'],
};

console.log(data1, data2);

问题分析

在上述代码中,data1 和 data2 的初始化存在问题:

  • data1 的 type 属性是 'setUserName',这是合法的,但 data 属性应该是一个字符串。
  • data2 的 type 属性是 'setUserList',这是合法的,但 data 属性应该是一个字符串数组。

data1报错如下 在这里插入图片描述 data2报错如下 在这里插入图片描述

解决方案

修正后的代码如下:

const data1: Data = {
  type: 'setUserName',
  data: 'tds',
};

const data2: Data = {
  type: 'setUserList',
  data: ['tds'],
};

console.log(data1, data2);

知识剖析

核心代码

type Data = {
  [T in keyof DataObj]: {
    type: T;
    data: DataObj[T];
  };
}[keyof DataObj];

代码解析

定义 DataObj 类型别名

首先定义了一个名为 DataObj 的类型别名,它描述了一个对象结构:

type DataObj = {
  setUserName: string;
  setUserList: string[];
  steUserInfo: { name: string };
};
  • setUserName 是一个字符串。
  • setUserList 是一个字符串数组。
  • steUserInfo 是一个包含 name 字段的对象,其中 name 是一个字符串。

定义Data 映射类型

接着定义了另一个名为 Data 的类型,这是一个映射类型,基于 DataObj 的键生成新的类型结构:

type Data = {
  [T in keyof DataObj]: {
    type: T;
    data: DataObj[T];
  };
}[keyof DataObj];

这里的关键点是:

  • keyof DataObj 获取 DataObj 的所有键名。

  • [T in keyof DataObj] 遍历 DataObj 的每一个键 T

  • {[T in keyof DataObj]:{ type: T; data: DataObj[T]; }}对于每个键T,创建一个新的类型成员,该成员是一个对象,包含两个属性:

    • type: T:表示当前键的名字。

    • data: DataObj[T]:表示与当前键关联的数据类型。

    • 此步骤生成的映射类型如下所示

      type DataTemp = {
      setUserName: {
         type: 'setUserName';
         data: string;
      };
      setUserList: {
         type: 'setUserList';
         data: string[];
      };
      steUserInfo: {
         type: 'steUserInfo';
         data: {
           name: string;
         };
      };
      };
      

      在这里插入图片描述

  • 最后的 [keyof DataObj] 将前面生成的映射类型转换为一个联合类型,包含了所有可能的键值对对象。

    • 此步骤生成的联合类型如下所示

      type Data = {
       type: "setUserName";
       data: string;
      } | {
       type: "setUserList";
       data: string[];
      } | {
       type: "steUserInfo";
       data: {
           name: string;
       };
      }
      

      在这里插入图片描述

涉及到的知识点

  • 泛型[3]
  • keyof 关键字[4]
  • in 关键字[4]
  • 索引类型与映射类型[3]

1. 泛型

泛型(Generics)允许你在定义函数、接口或类时使用一个或多个类型参数。这样做的好处是可以在不指定具体类型的情况下编写可重用的代码,同时保持类型安全。

语法

function identity<T>(arg: T): T {
  return arg;
}
  • <T> 表示这是一个类型参数。
  • T 可以是任何类型,当调用 identity 函数时,可以传入具体的类型来替代 T

例子

let output = identity<string>("myString"); // output 的类型为 string
let output2 = identity<number>(42); // output2 的类型为 number

2. keyof 关键字

keyof 关键字用于获取一个对象类型的键名的联合类型。这对于构建映射类型非常有用。

语法

type Keys = keyof { a: number, b: string };
// Keys 的类型为 "a" | "b"
  • keyof 会提取出 { a: number, b: string } 对象的所有键名,并形成一个联合类型 "a" | "b"

例子

interface Person {
  name: string;
  age: number;
}

type PersonKeys = keyof Person; // "name" | "age"

3. in 关键字

在 TypeScript 中,in 关键字用于遍历对象的键。它通常与映射类型结合使用,以生成新的类型结构。

语法

type MappedTypeWithNewProperties<T> = {
  [K in keyof T]: K extends string ? T[K] : never;
};
  • [K in keyof T] 遍历 T 的所有键 K
  • K extends string ? T[K] : never 是一个条件类型,表示如果 K 是字符串,则类型为 T[K];否则为 never

例子

type Person = {
  name: string;
  age: number;
};

type MappedPerson = {
  [K in keyof Person]: Person[K];
};

// MappedPerson 的类型为 { name: string; age: number; }

4. 索引类型

索引类型允许你通过键从一个对象中提取出相应的属性类型。这在处理动态键名时非常有用。

语法

interface StringArray {
  [index: number]: string;
}

const myArray: StringArray = ["a", "b", "c"];
const secondElement: string = myArray[1]; // 类型为 string
  • [index: number]: string 定义了一个索引签名,表示数组中的每个元素都是字符串。

例子

interface Dictionary {
  [key: string]: boolean;
}

const options: Dictionary = {
  isFlagged: true,
  isReadOnly: false,
};

const flag: boolean = options["isFlagged"]; // 类型为 boolean

5. 映射类型

映射类型是一种基于现有类型的键来生成新的类型的方式。它们通常用于自动地将一种类型的属性转换成另一种类型。

语法

type OptionsFlags<Type> = {
  [Property in keyof Type]: boolean;
};
  • [Property in keyof Type] 遍历 Type 的所有键 Property
  • boolean 是新类型中每个属性的类型。

例子

interface FeatureConfig {
  darkMode: () => void;
  newFeature: () => void;
}

type FeatureOptions = OptionsFlags<FeatureConfig>;

// FeatureOptions 的类型为 { darkMode: boolean; newFeature: boolean; }

总结

  • 泛型:允许你在定义函数、接口或类时使用类型参数,从而编写更通用和可重用的代码。
  • keyof 关键字:用于获取一个对象类型的键名的联合类型。
  • in 关键字:用于遍历对象的键,通常与映射类型结合使用。
  • 索引类型:允许你通过键从一个对象中提取出相应的属性类型。
  • 映射类型:基于现有类型的键来生成新的类型,通常用于自动地将一种类型的属性转换成另一种类型。

这些概念是 TypeScript 中高级类型系统的重要组成部分,能够帮助你编写更加灵活和类型安全的代码。

参考资料