以下是几个TypeScript 场景类面试题,涵盖了类型系统、泛型、高级类型、类型推导等高频考点,适合中高级前端或 TypeScript 开发者准备面试使用。
🧪 一、基础类型与结构兼容性
题目1:对象赋值兼容性
type A = { name: string };
type B = { name: string; age: number };
const a: A = { name: 'Tom' };
const b: B = { name: 'Jerry', age: 20 };
const x: A = b; // ✅ 合法吗?
const y: B = a; // ❌ 合法吗?
问题:这两行赋值是否合法?为什么?
✅ 答案:
x: A = b
✅ 合法。因为B
包含A
所需的所有属性(结构化类型)。y: B = a
❌ 不合法。因为缺少age
属性,无法满足B
的要求。
🔄 二、协变与逆变
题目2:函数参数和返回值的变体关系
type Animal = { name: string };
type Dog = Animal & { bark: () => void };
// 函数返回值是协变的
function getAnimal(): Animal {
return { name: 'Lily' };
}
function getDog(): Dog {
return { name: 'Max', bark: () => console.log('Woof!') };
}
const f1: () => Animal = getDog; // ✅ 合法吗?
const f2: () => Dog = getAnimal; // ❌ 合法吗?
// 函数参数是逆变的
function processDog(d: Dog): void {}
function processAnimal(a: Animal): void {}
const g1: (a: Animal) => void = processDog; // ❌ 合法吗?
const g2: (d: Dog) => void = processAnimal; // ✅ 合法吗?
问题:这些函数赋值是否合法?从协变/逆变的角度解释原因。
✅ 答案:
f1: () => Animal = getDog
✅ 合法。返回值是协变的,Dog
是Animal
的子类型。f2: () => Dog = getAnimal
❌ 不合法。返回值不能从父类型赋给子类型。g1: (a: Animal) => void = processDog
❌ 不合法。参数是逆变的,不能将接受更具体类型的函数赋给接受更宽泛类型的变量。g2: (d: Dog) => void = processAnimal
✅ 合法。参数是逆变的,可以将接受更宽泛类型的函数赋给接受更具体类型的变量。
这行代码在做什么?
const f1: () => Animal = getDog;
这是将函数 getDog
赋值给变量 f1
,并且显式地将 f1
的类型声明为:
() => Animal
也就是说,f1
是一个函数,调用它会返回一个符合 Animal
类型的对象。
而 getDog
是一个函数,调用它会返回一个符合 Dog
类型的对象。
✅ 为什么这个赋值是合法的?
TypeScript 使用结构化类型系统(structural typing),也就是说只要两个类型的结构兼容,就可以相互赋值。
我们来看这两个函数的返回类型:
getDog
返回的是:Dog
,即{ name: string; bark: () => void }
f1
声明的返回类型是:Animal
,即{ name: string }
由于 Dog
是 Animal
的"超集"------它包含了 Animal
所需的所有属性(至少有 name
),所以我们可以把返回 Dog
的函数赋值给一个期望返回 Animal
的变量。
🔄 从协变角度理解(Covariance)
函数的返回值类型是协变的(covariant)。这意味着:
如果
Dog
是Animal
的子类型,那么() => Dog
就可以被当作() => Animal
来使用。
换句话说:
() => Dog <= () => Animal
// Dog 函数可以赋值给 Animal 函数
这就是所谓的"协变"关系,在函数返回值中允许这种赋值。
💡 举个生活中的比喻
想象你去餐厅点了一杯 柠檬水(Animal) ,服务员端来了一杯 柠檬水 + 冰块(Dog)。
虽然比你要的多了一点东西(bark 方法),但本质上还是满足你的需求(name 属性),所以是可以接受的。
✅ 总结
表达式 | 含义 |
---|---|
() => Animal |
一个函数,调用后返回一个 Animal 类型的对象 |
getDog |
一个函数,调用后返回一个 Dog 类型的对象 |
const f1: () => Animal = getDog |
把返回更具体类型的函数赋值给返回更宽泛类型的变量 |
✅ 是否合法? | 是!因为 Dog 是 Animal 的子类型,返回值是协变的 |
🔁 三、联合类型与交叉类型
题目3:联合类型 vs 交叉类型
type A = { name: string };
type B = { age: number };
type U = A | B;
type I = A & B;
const u1: U = { name: 'Tom' }; // ✅
const u2: U = { age: 25 }; // ✅
const u3: U = { name: 'Jerry', age: 3 }; // ✅
const i1: I = { name: 'Alice' }; // ❌
const i2: I = { age: 40 }; // ❌
const i3: I = { name: 'Bob', age: 28 }; // ✅
问题:哪些赋值合法?说明联合类型和交叉类型的区别。
✅ 答案:
- 联合类型
U = A | B
表示"要么是 A,要么是 B",所以只需要满足其中一个即可。 - 交叉类型
I = A & B
表示"同时是 A 和 B",必须包含所有属性。 - 因此:
u1
,u2
,u3
都 ✅ 合法。i1
,i2
❌ 不合法,因为缺少必要属性。i3
✅ 合法。
📦 四、泛型与类型推导
题目4:实现一个通用的 map
类型转换函数
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
const result = [];
for (let i = 0; i < arr.length; i++) {
result.push(fn(arr[i]));
}
return result;
}
问题:写出这个函数的完整类型定义,并解释每个泛型的作用。
✅ 答案:
-
T
: 输入数组元素的类型。 -
U
: 经过fn
处理后的输出数组元素的类型。 -
这是一个典型的泛型函数,支持类型推导,例如:
const nums = [1, 2, 3];
const strs = map(nums, n => n.toString()); // 推导为 string[]
🧠 五、条件类型与映射类型
题目5:实现一个 RequiredKeys<T>
工具类型
目标:提取出接口中所有必须字段的键名。
例如:
type Example = {
id: number;
name?: string;
age: number;
};
type R = RequiredKeys<Example>; // 应该等于 "id" | "age"
问题:如何用 TypeScript 实现这个工具类型?
✅ 答案:
type RequiredKeys<T> = {
[K in keyof T]: {} extends Pick<T, K> ? never : K
}[keyof T];
解释:
Pick<T, K>
提取某个属性。{}
可以赋值给可选属性,但不能赋值给必填属性。- 利用这一点区分必填和可选字段。
🎯 六、实战场景题:实现一个深度只读类型
题目6:实现 DeepReadonly<T>
目标:将对象及其嵌套属性都设为只读。
type DeepReadonly<T> = ???;
type Example = {
name: string;
info: {
age: number;
hobbies: string[];
};
};
type ReadonlyExample = DeepReadonly<Example>;
// 所有属性都应变为 readonly
✅ 答案:
type DeepReadonly<T> = {
readonly [K in keyof T]:
T[K] extends object ? DeepReadonly<T[K]> : T[K]
};
注意: 这个实现对数组不会递归处理。如果要深度冻结数组元素,还需要额外判断数组类型。
🧩 七、进阶挑战:实现一个 Flatten<T>
类型
题目7:实现一个类型 Flatten<T>
,将嵌套对象展开成一层
type Input = {
a: number;
b: {
c: string;
d: {
e: boolean;
};
};
};
type Output = Flatten<Input>;
// 输出应为:
// {
// a: number;
// 'b.c': string;
// 'b.d.e': boolean;
// }
✅ 参考答案:
type Flatten<T> = {
[K in keyof T as T[K] extends object ? never : K]: T[K]
} & {
[K in keyof T as T[K] extends object ? K : never]: T[K] extends infer U
? U extends object
? Flatten<U>
: U
: never;
};
(这是一个较复杂的类型操作,适合高级开发者)
📚 总结:高频考点分类
类型 | 常见题目 |
---|---|
类型兼容性 | 对象赋值、函数参数/返回值 |
泛型 | 泛型函数、类型推导 |
条件类型 | extends ? true : false 、infer 使用 |
映射类型 | Partial , Required , Record 等自定义 |
联合与交叉 | ` |
深度操作 | DeepReadonly , Flatten |