一句话总结
TypeScript = JavaScript + 静态类型系统。它在源码中增加类型标注,由编译器在编译期做类型检查、生成智能提示,编译(或转译)后输出纯 JavaScript——类型只存在于编译期,运行时被完全擦除,所以 TS 没有额外运行时开销。它的核心价值不是"能发现所有 bug",而是:给大型项目提供接口契约、让 IDE 重构有安全保障、让"某个值到底是什么类型"从口头约定变成编译器可验证的事实。
# TS 的三个关键事实(面试常作开场追问):
# 1. 类型擦除:interface/type/泛型标注编译后全部消失,不存在"运行时的 TS 类型"
# 2. 结构化类型(鸭子类型):类型兼容看结构不看名字,class 没有私有字段时"形状相同即兼容"
# 3. 渐进式:可以和 JS 混用(allowJs),从 any 起步逐步收紧到 strict,不是全有或全无
初级理解
interface 与 type 的区别(最高频)
| 能力 | interface | type |
| 描述对象结构 | 可以 | 可以 |
| 声明合并(同名自动合并) | 可以 | 不可以 |
| 被 class implements | 可以 | 可以(对象类型部分) |
| extends 继承 | 可以(多接口继承) | 可以(交叉类型 &) |
| 联合/交叉/元组/映射/条件类型 | 不可以 | 可以 |
// interface:可扩展、可合并——适合描述"对象的形状"、对外契约
interface User {
id: number;
name: string;
}
interface User {
email?: string; // 声明合并:另一个文件/位置可继续扩展同一 interface
}
// type:表达力更强——联合类型、 mapped type、条件类型只有 type 能写
type ID = number | string; // 联合类型
type PartialUser = Partial<User>; // 映射类型
type NonNull<T> = T extends null | undefined ? never : T; // 条件类型
// 面试口径:能用 interface 就用 interface(可扩展性好、报错信息友好),
// 需要联合/元组/运算类型时用 type。两者在"对象结构"上大多数场景可互换。
any 与 unknown 的区别
// any:逃生舱——对 any 的任何操作都通过检查,等于关掉类型检查
let a: any = 'hello';
a.foo.bar.baz; // 不报错,运行时炸了才知道
// unknown:类型安全的 any——赋值什么都行,但使用前必须"收窄"
let u: unknown = 'hello';
u.toUpperCase(); // ✗ 报错:unknown 不能直接调用
if (typeof u === 'string') {
u.toUpperCase(); // ✓ 收窄后才能用
}
// 结论:unknown 比 any 安全——它强制你先证明值的类型再使用
函数类型与泛型基础
// 函数类型:参数必写类型(可被上下文推断时可省),返回值一般可推断
const add = (a: number, b: number): number => a + b;
// 泛型:类型的参数化——同一个函数/类型适配多种类型,同时保留具体类型信息
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // 推断 T = number,返回 number | undefined
const s = first(['a', 'b']); // T = string ——比返回 any 强太多
中级深入
类型推断是怎么工作的
# 1. 返回值推断:没有标注返回类型时,编译器从 return 语句推断联合类型
# 注意坑:if (x) return 1; return undefined; 会推断出 number | undefined,显式标注更稳
# 2. 上下文类型推断:把函数赋给有类型的变量/作为参数传入时,参数类型从上下文"倒推"
const on: (e: MouseEvent) => void = (e) => e.clientX; // e 自动是 MouseEvent
# 3. best common type:const x = cond ? 1 : 'a' 推断为 1 | 'a'(字面量)或 number | string
# as const 把一切收窄为字面量类型,常用于配置对象/路由表
泛型约束与 keyof T[K]
// extends 约束:只有具备某些能力的类型才能传入
function get<T extends { id: string }, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]; // 返回类型精确到 T[K],不是 any
}
get({ id: 'x', age: 1 }, 'age'); // 返回 number
// 面试追问:keyof typeof 的组合拳
const route = { home: '/', user: '/user' } as const;
type RouteKey = keyof typeof route; // 'home' | 'user'
type RoutePath = typeof route[RouteKey]; // '/' | '/user'
条件类型与 infer:工具类型的实现原理
// 条件类型:类型层面的三元表达式(模式匹配)
type IsString<T> = T extends string ? true : false;
// infer:在模式匹配中"捕获"类型片段——面试要求手写的高频点
type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
type MyParameters<F> = F extends (...args: infer P) => any ? P : never;
// 分发性:裸类型参数 + 联合类型会逐个分发
type ToArray<T> = T extends any ? T[] : never;
type R1 = ToArray<string | number>; // string[] | number[](不是 (string|number)[])
// 常用工具类型手写(面试常要求写出 2-3 个)
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
type MyOmit<T, K extends keyof any> = MyPick<T, Exclude<keyof T, K>>;
type MyRecord<K extends keyof any, V> = { [P in K]: V };
type MyExclude<T, U> = T extends U ? never : T; // 'a'|'b' 排除 'a' → 'b'
高级进阶
函数参数的逆变与协变
# TS 类型兼容的方向性(strictFunctionTypes 开启后):
# 协变:返回类型——子类型返回值可以赋给父类型返回位置(Dog 可当 Animal 用)
# 逆变:参数类型——接受 Animal 的函数可以赋给接受 Dog 的位置(方向反过来),更安全
interface Animal { name: string }
interface Dog extends Animal { bark(): void }
// 方法语法 vs 函数属性语法,检查规则不同(经典考点):
interface A1 { eat(d: Dog): void } // 方法语法:参数双变(宽松,历史上兼容)
interface A2 { eat: (d: Dog) => void } // 函数属性语法:参数逆变(strict 下更严格)
declare let f1: (d: Dog) => void;
declare let a1: A1; declare let a2: A2;
a1.eat = f1; // ✓ 方法语法允许
a2.eat = f1; // ✗ strictFunctionTypes 下报错(可能被传入非 Dog)
声明合并与类型空间
// interface 同名自动合并(扩展第三方库类型的标准手段:declare module)
// type/class 同名则报错——class 在类型空间是类型、在值空间是值(双空间是 TS 核心概念)
// declare global:在模块文件里扩展全局类型
declare global {
interface Window { __APP_CONFIG__: { apiBase: string } }
}
// import type:类型专用导入,编译后整行擦除——避免循环依赖、保证纯类型不产生运行时代码
import type { Config } from './config'; // 编译后此行消失
import { loadConfig } from './config'; // 这行才会保留
结构化类型的陷阱与"名义类型"模拟
// TS 默认结构化兼容:两个 class 字段形状相同就能互赋(没有私有成员时)
class Cat { name = '' }
class Dog { name = '' }
const c: Cat = new Dog(); // ✓(结构相同)——不想要就用 private/brand
// branding:用 never 品牌制造"名义类型"效果,防止 ID 混用
type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };
const uid = 'u1' as UserId;
const oid = 'o1' as OrderId;
// func(uid, oid) —— ✗ 编译期就拦住"两个都是 string 所以能混传"的经典事故
实战场景
JS 项目渐进迁移到 TS 的路线
# 1. 起步:npm i -D typescript @types/node;tsconfig 开 allowJs + checkJs,
# JS 文件里用 // @ts-check + JSDoc 注释就能享受类型检查,零文件改动
# 2. 新文件一律 .ts/.tsx;旧文件按模块价值排序逐个改后缀(改一个带动一片类型推导)
# 3. any 策略分级:禁止新增显式 any(noImplicitAny),历史遗留用 // eslint-disable 点状豁免
# 4. 边界收口:fetch/axios 封装、localStorage 读写、JSON.parse 这些"类型黑洞"集中治理
# 5. 最后开 strict 顶级开关,清完存量告警
tsconfig 关键配置项
{
"compilerOptions": {
"strict": true, // 一站式:noImplicitAny/strictNullChecks 等全家桶
"noUncheckedIndexedAccess": true, // arr[i] 是 T | undefined,防越界(可选,激进)
"esModuleInterop": true, // import React from 'react' 兼容 CJS 默认导出
"moduleResolution": "bundler", // Vite 项目推荐;老项目 node
"paths": { "@/*": ["src/*"] }, // 路径别名(只改类型解析,打包别名要配 Vite)
"skipLibCheck": true // 跳过 .d.ts 内部检查,大幅提速
}
}
axios 封装:让响应数据类型化
interface ApiResult<T> { code: number; message: string; data: T }
async function request<T>(url: string, init?: RequestInit): Promise<T> {
const res = await fetch(url, init);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = (await res.json()) as ApiResult<T>;
if (json.code !== 0) throw new Error(json.message);
return json.data; // 调用方拿到的就是精确的 T,不再是 any
}
// 使用:接口契约直达调用点
const user = await request<{ id: string; name: string }>('/api/user/1');
注意:类型断言(as)不是"转换",只是告诉编译器"闭嘴,我比你懂"。as any 会把类型检查链路整条打断,优先用 unknown + 收窄、泛型、类型守卫替代。
面试模拟
面试官:interface 和 type 有什么区别?什么时候必须用哪个?
你:两者都能描述对象结构,大多数场景可互换。interface 的独有能力是声明合并和被 class implements,适合描述对外契约、需要后续扩展的形状;type 的表达力更强,支持联合类型、元组、映射类型、条件类型——比如 number | string 这种联合只有 type 能写。实践上默认 interface,需要运算类型时用 type。
面试官:any 和 unknown 差在哪?
你:都能接收任意值,但 any 之后对它的任何操作都放行,等于关闭类型检查;unknown 是类型安全版,使用前必须先收窄——通过 typeof、instanceof、自定义类型守卫或 discriminate narrowing 证明类型后才能操作。所以 unknown 适合"外部输入"(接口响应、用户输入、localStorage 反序列化),any 适合"明知放弃"的逃生舱。
面试官:写一个 MyReturnType,说说 infer 的原理。
你:type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never。infer 是条件类型里的类型占位符:当 F 的结构能匹配 (...args: any[]) => R 这个模式时,返回位置的类型被捕获绑定到 R 上并返回。同类用法还有从数组元素 infer T[] 提取 T、从 Promise<T> 提取 T。注意裸类型参数参与条件类型时有分发性,联合类型会被逐个处理,不想要分发就用方括号包起来 [T] extends [U]。