我们经常会遇到这种需求,就是一个类型是依托于另外一个类型的。下面以具体的代码作为示例进行讲解
// 简单说来就是,type 为 dog 时是 run 方法,而 type 是 cat 时,则为 eat 方法
const animals: any[] = [{ type: 'dog', methods: { run: ()= {} }, { type: 'cat', methods: { eat: ()=> {}}]
为了解决这个问题,下面有三种方式可供参考
✅ 第一种:直接写联合类型
interface Dog {
type: "dog";
methods: { run: () => void };
}
interface Cat {
type: "cat";
methods: { eat: () => void };
}
type Union = Dog | Cat;
const _list: Union[] = [{ type: "dog", methods: { run: () => {} } }];
📚 专业术语:
| 项目 | 专业术语说明 |
|---|---|
| `Dog | Cat` |
type: "dog" 等 | ✅ 判别式联合(Discriminated Union)或标签联合(Tagged Union) |
🔍 特点:
- 适用于类型固定不变的场景;
- 适合配合
switch和if做类型缩窄(narrowing); - 是 TypeScript 类型建模最基础、最推荐的一种方式。
✅ 第二种:基于 key 映射的联合类型分发
// 这里做条件判断
type Methods<T> = T extends "dog" ? { run: () => void } : { eat: () => void };
type TypeKey = "dog" | "cat";
// 这里做自动生成联合类型
type AnimalUnion = {
[K in TypeKey]: {
type: K;
methods: Methods<K>;
};
}[TypeKey];
const _list2: AnimalUnion[] = [{ type: "cat", methods: { eat: () => {} } }];
重点讲一下AnimalUnion这个写法是什么意思
type AnimalUnion = {
[K in TypeKey]: {
type: K;
methods: Methods<K>;
};
}[TypeKey];
这是 映射类型 + 索引访问类型 组合使用的高级技巧,它的核心作用是:
根据联合类型
TypeKey = "dog" | "cat",构造出多个具名对象类型,再通过索引[TypeKey]把它们变成联合类型。
前提定义
type TypeKey = "dog" | "cat";
type Methods<T> = T extends "dog"
? { run: () => void }
: { eat: () => void };
🧩 步骤 1:[K in TypeKey] 是 映射类型
{
[K in TypeKey]: {
type: K;
methods: Methods<K>;
}
}
这会生成一个类型对象,类似于:
{
dog: {
type: "dog";
methods: { run: () => void };
};
cat: {
type: "cat";
methods: { eat: () => void };
};
}
注意:这里的 K 是泛型变量,值为 "dog" 和 "cat" 各走一轮,然后 Methods<K> 自动类型分发。
📦 步骤 2:[...] 是 索引访问类型,把对象类型转为联合类型
{
dog: {...};
cat: {...};
}["dog" | "cat"]
就等价于:
{
type: "dog";
methods: { run: () => void };
} | {
type: "cat";
methods: { eat: () => void };
}
也就是你要的:
type AnimalUnion =
| { type: "dog"; methods: { run: () => void } }
| { type: "cat"; methods: { eat: () => void } }
等价于手写的联合类型版本,但具有以下优势:
🧱 图示理解
// 中间类型结构:
{
dog: { type: "dog"; methods: { run: () => void } };
cat: { type: "cat"; methods: { eat: () => void } };
}
// 取联合:
{
...dog内容
} | {
...cat内容
}
✅ 第三种:泛型函数 + 类型映射
// 构造一个映射的 Map
interface AnimalMap {
dog: { run: () => void };
cat: { eat: () => void };
}
type AnimalType = keyof AnimalMap;
type AnimalMethods<T extends AnimalType> = AnimalMap[T];
// 设置统一泛型
interface Animal<T extends AnimalType = AnimalType> {
type: T;
methods: AnimalMethods<T>;
}
// 泛型函数来精确推导
// 错误写法❌,就当错误案例了,这种直接定义整个数组是不行的,会导致类型宽泛
function defineAnimals<T extends AnimalType>(
animals: Animal<T>[]
): Animal<T>[] {
return animals;
}
const _list3: Animal[] = defineAnimals([
{
type: "cat",
methods: {
eat: () => {}
}
},
{
type: "dog",
methods: {
eat: () => {} // 这里类型就不对了,你写一个没事,但是数组里一旦多出一项就会出问题
}
}
]);
// 正确写法✅
// 只针对每一项做泛型函数推导,就不会有问题
function defineAnimal<T extends AnimalType>(animal: Animal<T>): Animal<T> {
return animal;
}
const _list3: Animal[] = [
defineAnimal({ type: "cat", methods: { eat: () => {} } }),
defineAnimal({ type: "dog", methods: { run: () => {} } })
];
📚 专业术语:
| 项目 | 专业术语说明 |
|---|---|
Animal<T> | ✅ 泛型接口(Generic Interface) |
AnimalMap[T] | ✅ 索引访问类型(Indexed Access Types) |
defineAnimals | ✅ 泛型函数(Generic Function) |
推导 T | ✅ 类型参数推导(Type Inference) |
🔍 特点:
- 泛型推导让类型自动推断,更安全;
- 在库设计或工具函数中极为常见;
- 更强的类型可推导性(适合函数式设计)。
🔄 三者对比总结表格
| 实现方式 | 技术核心 | 主要术语 | 优势 | 适用场景 |
|---|---|---|---|---|
| 第一种:直接联合类型 | 枚举式联合 | 联合类型、判别式联合 | 简单直观,缩窄友好 | 类型数量少、结构固定 |
| 第二种:类型映射构造联合 | 条件分发 + 映射类型 | 条件类型、映射类型、分布式联合 | 动态映射、结构清晰 | 类型与 key 有明显对应 |
| 第三种:泛型函数 | 泛型 + 索引类型 | 泛型函数、索引访问类型 | 类型推导强、灵活 | 动态生成、函数设计 |