以下是几个TypeScript 场景类面试题,涵盖了类型系统、泛型、高级类型、类型推导等高频考点,适合中高级前端或 TypeScript 开发者准备面试使用。
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
的要求。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
的变量。
函数的返回值类型是协变的(covariant)。这意味着:
如果
Dog
是Animal
的子类型,那么() => Dog
就可以被当作() => Animal
来使用。
换句话说:
() => Dog <= () => Animal
// Dog 函数可以赋值给 Animal 函数
这就是所谓的“协变”关系,在函数返回值中允许这种赋值。
想象你去餐厅点了一杯 柠檬水(Animal),服务员端来了一杯 柠檬水 + 冰块(Dog)。
虽然比你要的多了一点东西(bark 方法),但本质上还是满足你的需求(name 属性),所以是可以接受的。
表达式 | 含义 |
---|---|
() => Animal |
一个函数,调用后返回一个 Animal 类型的对象 |
getDog |
一个函数,调用后返回一个 Dog 类型的对象 |
const f1: () => Animal = getDog |
把返回更具体类型的函数赋值给返回更宽泛类型的变量 |
✅ 是否合法? | 是!因为 Dog 是 Animal 的子类型,返回值是协变的 |
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
✅ 合法。map
类型转换函数function map(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[]
RequiredKeys
工具类型目标:提取出接口中所有必须字段的键名。
例如:
type Example = {
id: number;
name?: string;
age: number;
};
type R = RequiredKeys; // 应该等于 "id" | "age"
问题:如何用 TypeScript 实现这个工具类型?
✅ 答案:
type RequiredKeys = {
[K in keyof T]: {} extends Pick ? never : K
}[keyof T];
解释:
Pick
提取某个属性。{}
可以赋值给可选属性,但不能赋值给必填属性。DeepReadonly
目标:将对象及其嵌套属性都设为只读。
type DeepReadonly = ???;
type Example = {
name: string;
info: {
age: number;
hobbies: string[];
};
};
type ReadonlyExample = DeepReadonly;
// 所有属性都应变为 readonly
✅ 答案:
type DeepReadonly = {
readonly [K in keyof T]:
T[K] extends object ? DeepReadonly : T[K]
};
注意: 这个实现对数组不会递归处理。如果要深度冻结数组元素,还需要额外判断数组类型。
Flatten
类型Flatten
,将嵌套对象展开成一层type Input = {
a: number;
b: {
c: string;
d: {
e: boolean;
};
};
};
type Output = Flatten;
// 输出应为:
// {
// a: number;
// 'b.c': string;
// 'b.d.e': boolean;
// }
✅ 参考答案:
type Flatten = {
[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
: never;
};
(这是一个较复杂的类型操作,适合高级开发者)
类型 | 常见题目 |
---|---|
类型兼容性 | 对象赋值、函数参数/返回值 |
泛型 | 泛型函数、类型推导 |
条件类型 | extends ? true : false 、infer 使用 |
映射类型 | Partial , Required , Record 等自定义 |
联合与交叉 | ` |
深度操作 | DeepReadonly , Flatten |