面试-TypeScript 场景类面试题

以下是几个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 ✅ 合法。返回值是协变的,DogAnimal 的子类型。
  • 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 }

由于 DogAnimal 的"超集"------它包含了 Animal 所需的所有属性(至少有 name),所以我们可以把返回 Dog 的函数赋值给一个期望返回 Animal 的变量。


🔄 从协变角度理解(Covariance)

函数的返回值类型是协变的(covariant)。这意味着:

如果 DogAnimal 的子类型,那么 () => Dog 就可以被当作 () => Animal 来使用。

换句话说:

复制代码
() => Dog   <=   () => Animal
// Dog 函数可以赋值给 Animal 函数

这就是所谓的"协变"关系,在函数返回值中允许这种赋值。


💡 举个生活中的比喻

想象你去餐厅点了一杯 柠檬水(Animal) ,服务员端来了一杯 柠檬水 + 冰块(Dog)

虽然比你要的多了一点东西(bark 方法),但本质上还是满足你的需求(name 属性),所以是可以接受的。


✅ 总结

表达式 含义
() => Animal 一个函数,调用后返回一个 Animal 类型的对象
getDog 一个函数,调用后返回一个 Dog 类型的对象
const f1: () => Animal = getDog 把返回更具体类型的函数赋值给返回更宽泛类型的变量
✅ 是否合法? 是!因为 DogAnimal 的子类型,返回值是协变的

🔁 三、联合类型与交叉类型

题目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 : falseinfer 使用
映射类型 Partial, Required, Record 等自定义
联合与交叉 `
深度操作 DeepReadonly, Flatten
相关推荐
小赵学鸿蒙17 分钟前
用Uniapp开发鸿蒙项目 五
前端
小lan猫19 分钟前
【实战】 Vue 3、Anything LLM + DeepSeek本地化项目(五)
前端·vue.js
星使bling19 分钟前
基于Baidu JSAPI Three的卫星轨道三维可视化Demo
前端·javascript
Oder_C21 分钟前
自定义指令-优化v-if和v-show上的使用
前端·javascript·vue.js
小赵学鸿蒙22 分钟前
用Uniapp开发鸿蒙项目 八(上)
前端
拾光拾趣录23 分钟前
TypeScript 数组与对象类型定义
前端
小赵学鸿蒙23 分钟前
用Uniapp开发鸿蒙项目 四
前端
程序猿阿伟38 分钟前
《深入解析:如何通过CSS集成WebGPU实现高级图形效果》
前端·css
Monster411 小时前
鸿蒙性能引擎:ArkCompiler实战精要
前端
ze_juejin1 小时前
Typescript中的继承示例
前端·typescript