面试官都懵了!这 8 个 TypeScript 高级技巧直接让我 Offer 翻倍

33 阅读8分钟

前言

兄弟们,上周帮隔壁组 Review 代码,差点没把我气笑。打开项目一看,通篇的 any,把 TypeScript 玩成了 AnyScript。问他为啥不用类型,他说:“太麻烦了,定义不出来,不如直接 any 一把梭。”

卧槽,这牛逼! 你这是把 TS 当 JS 写,完全放弃了类型系统的编译时检查,不仅开发体验极差(没有自动补全),上线后还全是 undefined is not a function 的运行时报错。

其实,TypeScript 的类型系统极其强大,很多时候不是“定义不出来”,而是我们没掌握“类型体操”的姿势。今天我就把压箱底的 8 个 TypeScript 高级实战技巧 分享出来。这些技巧不仅能让你的代码健壮性提升一个档次,更是面试中装逼的神器

学会这些,下次面试官问你“TS 有什么高级用法”时,你直接甩出这几个案例,保证让他沉默!🔥{AB8DF469-481F-40BC-B58D-BB5CBB5E690F}.png


第一部分:为什么你觉得 TS 难用?

很多兄弟觉得 TS 难用,无非是遇到下面这两种情况:

  1. 后端数据结构太烂:后端返了一堆动态 key 的 JSON,你不知道怎么定义类型。
  2. 复用性差:写了一堆 interface,结果稍微变个字段又要重写一个,累觉不爱。

其实,TS 的核心威力在于“类型推导”和“类型变换” 。只要你掌握了如何从值反推类型,如何用现有类型生成新类型,写 TS 比写 JS 还爽!


第二部分:核心方案详解(实战干货)

1. 拒绝硬编码:巧用 keyof typeof 自动抓取键名 🚀

你是不是经常手动定义一堆字符串字面量类型?比如定义颜色、API 状态码。一旦常量改了,还得去改类型定义,很容易这就不同步了。

❌ 传统笨办法:

const colors = {
  red: 'reddish',
  blue: 'bluish'
};

// 每次改 colors 对象,还得手动改这里,累不累?
type Colors = 'red' | 'blue';

✅ 高手方案:

直接利用 typeof 捕获变量类型,再用 keyof 提取键名。代码直接复制就能跑:

const colors = {
  red: '#ff0000',
  blue: '#0000ff',
  green: '#00ff00'
};

// 🔥 卧槽,一行代码搞定,自动同步!
type Colors = keyof typeof colors;

// 测试一下
let color: Colors;
color = 'red';  // ✅ OK
color = 'blue'; // ✅ OK
// color = 'yellow'; // ❌ 报错:Type '"yellow"' is not assignable to type '"red" | "blue" | "green"'.

原理: typeof 获取了 colors 对象的整体结构,keyof 提取了该结构的键名联合类型。这在处理配置项、枚举映射时简直是神器,。{184A9670-4D93-4D50-97FD-277EEE6CF7A5}.png


2. 自定义类型守卫 (User Defined Type Guards) 🔥

我们在处理联合类型时,经常需要判断具体是哪种类型。普通的 typeof 往往不够用,特别是处理接口响应时。

❌ 痛点场景:

interface Foo { foo: number; common: string; }
interface Bar { bar: number; common: string; }

function doStuff(arg: Foo | Bar) {
  if (arg.foo) { // ❌ 报错:Property 'foo' does not exist on type 'Bar'.
    console.log(arg.foo);
  }
}

✅ 高手方案:使用 is 关键字

我们可以写一个函数,告诉编译器:“相信我,如果这个函数返回 true,它就是 Foo 类型”。

// 🚀 定义一个类型守卫函数
function isFoo(arg: any): arg is Foo {
  return arg.foo !== undefined;
}

function doStuff(arg: Foo | Bar) {
  if (isFoo(arg)) {
    // ✨ 在这个块里,TS 知道 arg 必定是 Foo,放心用!
    console.log(arg.foo); // ✅ OK
    // console.log(arg.bar); // ❌ Error,这里不可能是 Bar
  } else {
    // ✨ 这里的 else 自动推导为 Bar,太智能了!
    console.log(arg.bar); // ✅ OK
  }
}

原理: arg is Foo 是类型谓词。它通过运行时的检查,实现了编译时的类型收窄,。

{2BD7A973-2639-4E2E-8DFA-81632F314FEE}.png


3. 彻底消灭 any:泛型 + 类型推导 (Generics)

写通用组件或函数时,不要用 any!泛型就是为了解决“既要通用,又要类型安全”的问题。

🔥 实战场景:一个通用的 API 请求函数

// 定义一个泛型函数,T 代表返回的数据结构
function request<T>(url: string): Promise<T> {
  return fetch(url).then(response => response.json());
}

interface User {
  id: number;
  name: string;
}

// 💥 使用时传入具体类型,瞬间获得自动补全!
request<User>('/api/user/1').then(user => {
  console.log(user.name); // ✅ 敲 user. 就会自动提示 name
  // console.log(user.age); // ❌ 报错,User 接口里没 age
});

原理: 泛型 <T> 就像是一个占位符,调用时才填入具体类型。这样既保证了函数的通用性,又保留了严格的类型检查。{454BF958-AA8F-4591-99A1-7B3EB8737B55}.png


4. 字符串枚举的替代方案 (String Literals Union)

TypeScript 的 Enum 有时候编译出来的代码比较冗余。很多时候,我们其实只需要一组字符串常量。

✅ 高手方案:组合字面量类型

// 定义一组字面量联合类型
type CardinalDirection = "North" | "East" | "South" | "West";

function move(distance: number, direction: CardinalDirection) {
    console.log(`Moving ${distance} meters towards ${direction}`);
}

move(1, "North"); // ✅ OK
// move(1, "Nurth"); // ❌ 报错:拼写错误直接拦截!

这种写法编译后没有任何运行时开销(就是纯字符串),性能最好,也是目前很多开源库推荐的写法,。


5. 只读属性 Readonly:让数据不可变

在 React 或 Redux 开发中,不可变数据(Immutable)非常重要。防止队友手贱修改了 Props 或 State,我们可以用 Readonly

interface User {
  readonly id: number; // 👈 重点在这里
  name: string;
}

const u: User = { id: 1, name: 'Jack' };

u.name = 'Rose'; // ✅ OK
// u.id = 2; // ❌ 报错:Cannot assign to 'id' because it is a read-only property.

进阶: 如果想让整个对象都只读,可以用 Readonly<T> 映射类型:

type ReadonlyUser = Readonly<User>;
// 现在 id 和 name 都不能改了!

这在维护大型项目时,能避免很多莫名其妙的 Bug,。{92EE60BC-8013-466E-BB71-8FECD386C00A}.png


6. 穷尽性检查 (Exhaustive Check):利用 never 兜底 🛡️

这是我面试最爱问的一个点。当你用联合类型(Union Type)做状态管理时,如何保证你在 switchif 中处理了所有可能的情况?如果有人新增了一个状态,怎么让编译器提醒你去改代码?

🔥 神技:利用 never 类型

interface Square { kind: "square"; size: number; }
interface Rectangle { kind: "rectangle"; width: number; height: number; }
interface Circle { kind: "circle"; radius: number; } // 假如这是后来新增的

type Shape = Square | Rectangle | Circle;

function area(s: Shape) {
  switch (s.kind) {
    case "square": return s.size * s.size;
    case "rectangle": return s.width * s.height;
    case "circle": return Math.PI * s.radius ** 2;

    default:
      // 🔥 这里的检查是关键!
      // 如果你忘记写 case "circle",s 的类型在这里就不会是 never,TS 就会报错!
      const _exhaustiveCheck: never = s;
      return _exhaustiveCheck;
  }
}

原理: never 表示永远不存在的类型。如果 switch 把所有可能的 kind 都处理了,default 分支里的 s 就应该是 never。如果有遗漏,s 就会剩下某种类型,导致赋值给 never 报错,。 {0F3ED9A8-4208-48A9-A749-56C714F3CE77}.png


7. 品牌类型 (Nominal Typing):防止类型混用

TS 是“结构化类型”系统(Duck Typing),只要结构一样就认为是同一个类型。但有时候我们希望区分它们,比如 UserIdOrderId 都是字符串,但不能混用。

✅ 高手方案:使用“品牌”标记(Branding)

// 利用交叉类型 & 给字符串打个“标签”
type UserId = string & { readonly _brand: unique symbol };
type OrderId = string & { readonly _brand: unique symbol };

// 模拟构造函数
function UserId(id: string) { return id as UserId; }
function OrderId(id: string) { return id as OrderId; }

const myUser = UserId('user_01');
const myOrder = OrderId('order_01');

// myUser = myOrder; // ❌ 报错!虽然本质都是 string,但 TS 认为它们类型不同!

这种技巧在处理复杂的 ID 逻辑、货币计算时非常有用,能防止低级的业务逻辑错误。{5D067F03-C83E-415C-8A39-B5A262FC615C}.png


8. 巧用 Record 构建映射对象

当你需要定义一个对象,Key 是某种字符串枚举,Value 是另一种类型时,别再手写索引签名了。

❌ 啰嗦写法:

type Page = 'home' | 'about' | 'contact';

interface PageInfo {
  title: string;
}

// 如果 Page 加了一个类型,这里忘了加怎么办?
const nav: { [key: string]: PageInfo } = {
  home: { title: 'Home' },
  // ...
};

✅ 高手方案:Record<K, T>

type Page = 'home' | 'about' | 'contact';

interface PageInfo {
  title: string;
}

// 🔥 Record 会强制你必须包含 Page 中定义的所有 Key,少写一个都会报错!
const nav: Record<Page, PageInfo> = {
  home: { title: 'Home' },
  about: { title: 'About' },
  contact: { title: 'Contact' }
};

这能保证你的配置对象和类型定义永远保持同步,少写 bug。


第三部分:进阶扩展——类型体操的魅力

把上面这些技巧组合起来,你就能写出非常强大的泛型工具。

比如,Redux 的类型定义 就是这些技巧的集大成者。它利用了 Discriminated Unions(可辨识联合) 来处理 Action,利用 Generics(泛型) 来推导 State 的变化。

看看这个 Redux 的简化版类型定义,是不是很眼熟?

// Action 是一个联合类型,依靠 type 字段区分
type Action =
  | { type: 'INCREMENT' }
  | { type: 'DECREMENT' }
  | { type: 'SET', payload: number };

function reducer(state: number = 0, action: Action) {
  switch (action.type) {
    case 'INCREMENT': return state + 1; // ✅ 自动推导
    case 'SET': return action.payload; // ✅ TS 知道只有 SET 才有 payload
    default: return state;
  }
}

掌握了这些,你在看开源库源码(如 Redux, React-Router)时,就不会再对着那一堆 <T, U, V> 发呆了,。{6F62F2B8-8BBB-43FA-91CD-FECEB38720C0}.png


第四部分:总结

类型优化不是玄学,就是细节的积累

  1. 多用 keyof typeof,少写硬编码。
  2. 善用 is 类型守卫,运行时和编译时两手抓。
  3. 泛型 <T> 是灵魂,拒绝 any
  4. never 兜底,让 Bug 在编译阶段就现原形。

未来趋势预测:随着 TypeScript 5.x 的迭代,类型推导会越来越智能,但核心的结构化类型(Structural Typing)代数数据类型(ADT) 的思想不会变。掌握这些,你离高级工程师只差一个项目的距离!

{C947D877-2C7B-4D4B-9E03-EE5E284C9F62}.png


最后:

觉得有用的话,点赞、收藏、关注三连走一波!🔥 你的支持是我更新的动力!

收藏这篇,随时回来复习,下次面试直接抄走!