一、为什么需要 TypeScript?
JavaScript 是动态类型语言,变量类型在运行时才能确定。这带来了灵活性,但也容易写出意外错误:比如把数字当作函数调用,或者访问对象上不存在的属性。
TypeScript = JavaScript + 静态类型系统 。在写代码时给变量标注类型,TS 编译器会在编译阶段检查类型是否匹配,将错误提前暴露出来。
另外,类型注解是一份天然的"文档",编辑器能利用它给出精准的智能提示,极大提升开发效率。
注意:TypeScript 的类型只在编译时存在,编译后的 JS 代码中全部类型会被擦除,不会影响运行。
二、基础类型:给数据贴上明确的标签
1. 原始类型
概念:与 JavaScript 的七种原始类型对应(string, number, boolean, null, undefined, symbol, bigint)。
为什么需要:让编译器知道变量只能赋特定类型值,防止意外。
typescript
let name: string = "Alice"; // name 只能是字符串
let age: number = 30; // age 只能是数字
let isDone: boolean = false; // isDone 只能是布尔值
let u: undefined = undefined;
let n: null = null;
let sym: symbol = Symbol("key");
let big: bigint = 100n;
注意 :
null和undefined是所有类型的子类型(在strictNullChecks关闭时),但建议开启strict以强制区分,避免出错。
2. 数组
概念:存储一组相同类型的数据。
typescript
let list: number[] = [1, 2, 3]; // 元素只能是数字
let list2: Array<number> = [1, 2, 3]; // 泛型写法,等价
原理 :
number[]语法更简洁,Array<number>是使用泛型接口。两者编译结果相同。
3. 元组 (Tuple)
概念:一个已知元素数量和各自类型的数组,各位置类型可不同。
为什么需要:函数返回多个不同值、坐标点等固定结构。
typescript
let tuple: [string, number] = ["Alice", 25];
// 访问 tuple[0] 是 string,tuple[1] 是 number
// 越界访问会报错,比如tuple[2]会报错
tuple.push(null)
console.log(tuple)// [ 'Alice', 25, null ]
注意 :元组长度是固定的,但
push等操作目前无法被完全限制(历史遗留),尽量避免。
4. 枚举 (Enum)
概念:一组有名字的常量。默认数字枚举,也可以字符串枚举。
为什么需要:代替魔法数字或字符串,让代码更可读。
typescript
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
let dir: Direction = Direction.Up;
字符串枚举不给数值,但每个成员必须初始化:
typescript
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE"
}
原理 :编译后,枚举会生成一个双向映射的对象(数字枚举),字符串枚举则为单向对象。使用
const enum编译时不生成对象,直接内联值,性能更优。
注意:枚举不是 TypeScript 的类型级概念,它同时产生类型和运行时值。在需要常量集合时使用,否则可用联合字符串类型替代。
5. any、unknown、void、never
这些类型具有特殊含义:
- any :放弃类型检查,与普通 JS 一样。应当少用,因为它破坏了类型安全。
- unknown :安全的"any"。表示任意类型,但使用前必须用类型守卫收窄。
- void :函数没有返回值(返回
undefined)。 - never:永远不会有返回值的函数(总是抛出异常或死循环)。
typescript
let x: any = 10;
x.toUpperCase(); // 编译器不报错,但运行时可能出错
let y: unknown = 10;
if (typeof y === "string") {
y.toUpperCase(); // 必须检查类型后才能用
}
function warn(): void {
console.log("warning");
}
function error(): never {
throw new Error("出错啦!");
}
原理 :
unknown是类型安全的顶类型(所有类型都可以赋给它,但它是不可操作的),any既是顶类型又是底类型(它可赋给任何类型,不检查),never是底类型(所有类型的子类型,没有具体值)。这个层级叫做类型兼容性。
三、接口(Interface)与类型别名(Type)
1. 接口 Interface
概念:定义一个对象的形状(有哪些属性,分别是什么类型)。
为什么需要:为对象结构提供契约,保证对象拥有规定的属性和方法。
typescript
interface User {
name: string;
age: number;
readonly id: number; // 只读,创建后不能改
email?: string; // 可选属性
}
const user: User = { name: "Alice", age: 25, id: 1 };
user.id = 2; // 报错!
原理 :TypeScript 采用结构类型系统(鸭子类型),只要对象的结构满足接口要求即可,不需要显式声明实现。
2. 类型别名 Type
概念:给一个类型起个新名字,可以是任意类型。
typescript
type Point = {
x: number;
y: number;
};
type ID = string | number;
3. Interface 与 Type 的区别(面试重点)
| Interface | Type | |
|---|---|---|
| 扩展方式 | extends 继承 |
交叉类型 & |
| 重复定义 | 同名接口自动合并 | 同名 type 报错 |
| 适用范围 | 主要描述对象、函数等 | 任何类型(联合、元组、映射等) |
| 性能 | 接口可以被缓存,更适合扩展 | 类型别名只是别名 |
typescript
// 接口合并
interface A { x: number; }
interface A { y: number; } // 合并 => { x: number; y: number; }
// 类型别名不支持合并
type B = { x: number; };
type B = { y: number; }; // 报错
建议 :优先使用
interface描述对象形状,当需要联合类型、重命名基本类型、元组等时用type。
四、联合类型与交叉类型
1. 联合类型 (Union)
概念 :一个值可以是几种类型之一,用 | 分隔。
为什么需要 :处理不确定的类型,如 string | number 的 ID。
typescript
let id: string | number = "abc";
id = 123; // 合法
注意 :联合类型只能访问所有类型共有的属性。要使用特有属性,需先类型收窄(见下节)。
2. 交叉类型 (Intersection)
概念 :将多个类型合并成一个类型,用 & 连接。
为什么需要:组合多个对象或接口的特性。
typescript
interface A { a: number; }
interface B { b: string; }
type C = A & B; // { a: number; b: string; }
原理:交叉类型是求并集(需同时满足),联合类型是求或集(满足其中一个即可)。交叉常用于合并多个对象类型,如混入模式。
2.1. 同名属性类型冲突 → 推导为 never
这是最常见的报错来源。当两个类型中同名属性的类型没有交集 时,交叉后该属性的类型变成 never。
typescript
interface X {
value: string;
}
interface Y {
value: number;
}
type Z = X & Y;
// Z 的类型相当于 { value: string & number } => { value: never }
原理 :string & number 没有哪个值能同时是 string 又是 number,所以交点为空,即 never。实际使用时,你会收到类型错误:
typescript
const obj: Z = {
value: "hello" // ❌ 报错:不能将类型 'string' 分配给类型 'never'
};
或者试图赋值时直接报错:
typescript
const test: Z = { value: 1 }; // ❌ 同样错误
2.2. 函数类型的交叉
如果冲突发生在函数类型上,交叉会形成函数重载 ,这通常不会报错,但调用时可能会限制为 never(取决于参数)。
typescript
type Fn1 = (x: string) => void;
type Fn2 = (x: number) => void;
type Fn = Fn1 & Fn2;
// 相当于 (x: string & number) => void => (x: never) => void
此时调用该函数:
typescript
declare const fn: Fn;
fn("hello"); // ❌ 报错:类型 'string' 的参数不能赋给类型 'never'
fn(123); // ❌ 同样错误
函数交叉通常用于重载签名,正确的做法是使用函数重载声明或泛型。
2.3. 对象类型与方法签名的冲突
typescript
interface Logger {
log(msg: string): void;
}
interface Printer {
log(msg: number): void;
}
type Hybrid = Logger & Printer;
// log 的类型变成 ((msg: string) => void) & ((msg: number) => void)
// 即 (msg: string & number) => void -> (msg: never) => void
调用时会报错,因为不能同时是 string 和 number。
2.4. 使用条件类型检测 never
typescript
type IsNever<T> = [T] extends [never] ? true : false;
type Test = IsNever<Z['value']>; // true
2.5. 何时交叉类型不会报错?
- 同名属性的类型兼容(如
string和string,或{a:number}和{b:string}合并成{a:number; b:string})。 - 属性和方法签名兼容(同一类型或子类型)。
- 使用交叉扩展已有接口(如
type Extended = Base & { extra: boolean })。
2.6. 总结:交叉类型报错的根本原因
交叉类型本质上求的是"类型的交集" ,当交集为空时(即无法找到一个值同时满足两边),就会表现为 never,从而导致赋值错误。这是 TypeScript 保证类型安全的重要机制------它不会让你创造"既要求是 string 又要求是 number"的变量,因为那不可能存在。
五、类型守卫与类型收窄
概念:TypeScript 会分析代码逻辑,在条件判断后自动缩小变量的类型范围。
为什么需要:安全地使用联合类型中的特定方法。
typescript
function printId(id: string | number) {
if (typeof id === "string") {
// 这里 id 被收窄为 string
console.log(id.toUpperCase());
} else {
// 这里 id 为 number
console.log(id.toFixed(2));
}
}
常用守卫:
typeof:用于原始类型instanceof:用于类实例in:检查属性是否存在- 自定义类型谓词:
arg is Type形式
typescript
interface Cat { meow(): void; }
interface Dog { bark(): void; }
function isCat(animal: Cat | Dog): animal is Cat {
return (animal as Cat).meow !== undefined;
}
if (isCat(pet)) {
pet.meow(); // 这里 pet 是 Cat
}
原理 :
animal is Cat是类型谓词,函数返回true时,TypeScript 会将参数类型收窄为Cat。这是从任意联合类型中提取类型的重要手段。
1. 什么是类型谓词?
类型谓词是 TypeScript 中一种特殊的返回值类型,语法为 parameterName is Type。它用于自定义类型守卫 (User-Defined Type Guard)。当一个函数的返回类型是类型谓词时,TypeScript 会根据函数的 true/false 结果,在后续代码中自动收窄参数的类型。
简单说,就是告诉编译器:"如果这个函数返回 true,那么参数就是某个特定类型"。
2. 为什么需要自定义类型守卫?
TypeScript 的内置类型收窄(如 typeof、instanceof、in)只能处理一些简单情况。当联合类型中的多个类型具有不同的结构时,我们可以自己编写更精确的判断函数。
例如, 有 Cat 和 Dog 两个接口:
typescript
interface Cat {
meow(): void;
}
interface Dog {
bark(): void;
}
现在有一个函数接收 Cat | Dog 类型的参数,我们想调用 meow(),但直接调用会报错,因为 Dog 上没有 meow 方法:
typescript
function makeSound(animal: Cat | Dog) {
animal.meow(); // ❌ 报错:类型"Cat | Dog"上不存在属性"meow"
}
我们需要先判断它是 Cat 还是 Dog。可以使用 in 操作符:
typescript
if ("meow" in animal) {
animal.meow(); // 这里 animal 被收窄为 Cat
}
但有时候判断逻辑更复杂,或者想要更清晰的语义,就可以使用自定义类型守卫。
3. 详解 isCat 函数
typescript
function isCat(animal: Cat | Dog): animal is Cat {
return (animal as Cat).meow !== undefined;
}
- 参数类型 :
animal: Cat | Dog,表示传入的 animal 可能是 Cat 或 Dog。 - 返回类型 :
animal is Cat,这是一个类型谓词。它告诉 TypeScript:"如果这个函数返回true,那么参数animal就是Cat类型"。 - 函数体 :
(animal as Cat).meow !== undefined。先用as Cat进行类型断言,将 animal 当作 Cat 来访问meow属性,然后判断它是否不等于undefined。如果是,说明 animal 拥有meow方法,推断它是 Cat。
为什么这样做?
我们无法直接访问联合类型中不共有的属性,所以用类型断言临时"假设"它是 Cat,然后检查特有属性 meow 是否存在。这比 in 操作符更显式,适合复杂检查。
4. 使用 isCat 后的效果
typescript
function interact(animal: Cat | Dog) {
if (isCat(animal)) {
// 在这个分支里,animal 被自动收窄为 Cat
animal.meow(); // ✅ 合法
} else {
// 这里 animal 被收窄为 Dog
animal.bark(); // ✅ 合法
}
}
正确和错误调用的例子:
typescript
const cat: Cat = { meow: () => console.log("Meow!") };
const dog: Dog = { bark: () => console.log("Woof!") };
// ✅ 正确使用
if (isCat(cat)) {
cat.meow(); // 输出 "Meow!"
}
if (!isCat(dog)) {
dog.bark(); // 输出 "Woof!"
}
// ❌ 错误使用:不能将 Cat 赋给 Dog 或反过来
// isCat 只接受 Cat | Dog,传入其他类型会报错
isCat(42); // 报错:类型"number"的参数不能赋给类型"Cat | Dog"的参数
注意 :自定义类型守卫只是编译时的类型收窄,不会在运行时保证类型安全。如果函数体实现有误(例如总是返回
true),可能会导致运行时错误。所以必须确保守卫函数的逻辑是正确的。
六、函数类型
概念:指定函数的参数类型和返回值类型。
为什么需要:明确调用方式,防止传入错误参数。
typescript
function add(x: number, y: number): number {
return x + y;
}
//正确调用
add(1, 2); // 返回 3
add(0.5, -3); // 返回 -2.5
add(100, 200); // 返回 300
//错误调用及原因:
add(1); // ❌ 缺少第二个参数
add(1, 2, 3); // ❌ 多余参数
add("1", 2); // ❌ 第一个参数类型错误:string 不能赋给 number
add(1, "2"); // ❌ 第二个参数类型错误
add(true, false); // ❌ boolean 不能赋给 number
//解释:`add` 的参数和返回值都明确为 `number`,TypeScript 会严格检查参数数量和类型。多传或少传都会报错
// 可选参数与默认值
function greet(name: string, title?: string): string {
return title ? `${title} ${name}` : name;
}
//正确调用:
greet("Alice"); // 返回 "Alice"
greet("Bob", "Dr."); // 返回 "Dr. Bob"
greet("Charlie", ""); // 返回 "Charlie"(因为空字符串是 falsy,取 name)
greet("Diana", "Professor"); // 返回 "Professor Diana"
//错误调用:
greet(); // ❌ 缺少必选参数 name
greet(123); // ❌ name 必须是 string
greet("Eve", "Ms.", "extra"); // ❌ 多余参数
greet(true, false); // ❌ 类型错误
greet("Frank", 5); // ❌ title 只能是 string 或 undefined
//解释:`title?` 表示可选参数,可以传递 `string` 或 `undefined`,但不能传其他类型。在调用时,可选参数可以省略,但必选参数 `name` 必须提供。
// 剩余参数
function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
//正确调用:
sum(); // 返回 0(空数组求和)
sum(1); // 返回 1
sum(1, 2, 3); // 返回 6
sum(10, 20, 30, 40); // 返回 100
//错误调用:
sum(1, 2, "3"); // ❌ "3" 不是 number
sum([1, 2, 3]); // ❌ 传入的是数组,不是展开的数字列表
sum(null); // ❌ null 不能赋给 number
sum(undefined); // ❌ undefined 不能赋给 number
//解释:`...nums: number[]` 表示收集所有剩余参数到一个 `number[]` 数组。调用时必须传入零个或多个 `number`,不能传入数组整体(除非使用展开语法 `sum(...[1,2,3])`,但那是将数组展开成参数)。传入非数字类型会报错。
注意:可选参数必须放在必选参数之后。函数重载(多个函数头签名)可以精确描述多种调用方式。
typescript
// 函数重载:声明多种调用形式
function combine(a: string, b: string): string;
function combine(a: number, b: number): number;
// 实现签名
function combine(a: any, b: any): any {
return a + b;
}
combine("Hello ", "World"); // 返回 string
combine(1, 2); // 返回 number
// combine("a", 2); // 报错!没有匹配的重载
原理:重载声明只是类型层面的信息,实现签名必须兼容所有重载声明。编译器根据传入参数类型选择对应声明检查。
6.1、为什么要函数重载?
JavaScript 中经常有一些函数,根据传入参数的类型或数量,执行不同的逻辑,返回不同类型。例如 Array.prototype.slice:
- 不传参数时返回数组的浅拷贝(数组)
- 传入数字时返回子数组(数组)
如果用 TypeScript 写这个函数,我们希望:
typescript
function slice(): number[];
function slice(start: number): number[];
function slice(start: number, end: number): number[];
三种调用方式返回的都是 number[],但这里签名已经多了。再比如一个 padLeft,传入数字和字符串的行为完全不同:
typescript
padLeft("hello", 10); // 返回 " hello",期望类型是 string
padLeft(123, "0"); // 返回 "00123",期望类型也是 string
虽然看起来简单,但当参数类型决定返回值类型,或者参数数量不同导致处理方式不同时,函数重载就能精确地捕获这些调用模式,提供更准确的类型检查。
没有重载,你只能写一个笼统的实现,用 any 或联合类型,这样会丢失调用时对参数约束的精度。
6.2、示例代码详解
6.2.1. 重载声明
来看回上面的代码
typescript
function combine(a: string, b: string): string;
function combine(a: number, b: number): number;
这两行被称为重载签名 ,它们定义了 combine 函数的两种合法调用方式:
- 传入两个
string,返回string - 传入两个
number,返回number
这两个签名只用于类型检查,不包含函数体。TypeScript 会记住这些调用模式。
6.2.2. 实现签名
typescript
function combine(a: any, b: any): any {
return a + b;
}
这是唯一真正的函数体,被称为实现签名 。它的参数类型和返回类型必须兼容 所有重载签名(通常用更宽泛的类型如 any、联合类型等来涵盖所有可能)。
在这里,a 和 b 是 any,返回值也是 any,因为实际运行时可能返回 string 或 number。函数体中的 a + b 根据 JavaScript 的 + 规则:字符串拼接或数字相加。
6.2.3. 实际调用与类型检查
typescript
combine("Hello ", "World"); // 返回 string,类型推断为 string
combine(1, 2); // 返回 number,类型推断为 number
这两次调用分别匹配第一个和第二个重载签名。TypeScript 根据传入参数的类型,选择对应的重载签名,并知道返回值的精确类型。
6.2.4. 错误调用
typescript
// combine("a", 2); // ❌ 报错:没有匹配的重载
为什么报错?
因为 combine("a", 2) 的参数是 string 和 number,既不符合 (string, string),也不符合 (number, number)。TypeScript 在重载签名列表中逐个检查,找不到匹配项,于是抛出错误。即使实现签名用了 any,实现签名也不作为直接调用的类型签名,它只用来检查函数体是否兼容所有重载签名。
6.3、重载的规则和原理
- 重载签名写在实现签名前面,而且之间不能有函数体。
- 实现签名不可见给外部调用者。外部调用时只能看到重载签名列表。
- 实现签名的参数与返回类型必须兼容所有重载签名 。通常使用联合类型或
any来涵盖。 - 匹配时按顺序检查,第一个匹配的签名胜出(所以更具体的签名放在前面)。
6.4、生活类比
想象一个"榨汁机":
- 如果你放入苹果,它给你苹果汁。
- 如果你放入橙子,它给你橙汁。
- 你不能放入石头,因为榨汁机不接受石头。
重载签名就是这台榨汁机的使用说明书,上面写着:可以放苹果(返回苹果汁),可以放橙子(返回橙汁)。实现则是机器内部的刀片,管你放什么进去,它都会切,但说明书(类型系统)限制了你只能放苹果或橙子。
6.5、更多重载示例
例1:根据参数类型返回不同结果
typescript
function getInfo(id: number): { id: number; name: string };
function getInfo(name: string): { id: number; name: string }[];
function getInfo(param: number | string): any {
if (typeof param === "number") {
return { id: param, name: "Alice" };
} else {
return [{ id: 1, name: param }];
}
}
getInfo(1); // 返回 { id: 1, name: 'Alice' }
getInfo("Bob"); // 返回 [{ id: 1, name: 'Bob' }]
例2:参数个数不同
typescript
function print(value: string): void;
function print(value: string, times: number): void;
function print(value: string, times?: number): void {
const repeat = times ?? 1;
for (let i = 0; i < repeat; i++) {
console.log(value);
}
}
print("Hello"); // 输出一次
print("World", 3); // 输出三次
七、泛型(Generics)------ 写出灵活又安全的代码
概念
泛型就像是"类型变量",在定义函数、接口或类时不预先指定具体类型,而在使用的时候再确定。
生活类比:一个空盒子,你可以放进苹果,也可以放进书。盒子本身不限制内容,但当你放进去之后,它就确定了内容类型。
为什么需要泛型?
- 保持代码复用性:同一个函数可以处理不同类型的数据。
- 保证类型安全:使用泛型后,传入和返回的类型保持一致,不会丢失类型信息。
基础用法
typescript
function identity<T>(arg: T): T {
return arg;
}
// 调用方式1:显式指定类型
let output1 = identity<string>("hello");
// 调用方式2:类型推断
let output2 = identity(123); // output2 为 number 类型
这里 <T> 就是泛型变量,可以理解为"类型的占位符"。identity 函数接收任意类型 T,并返回同类型值。
泛型约束
有些时候,你想限制泛型参数必须具有某些属性,比如有 length 属性。使用 extends 关键字:
typescript
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength("abc"); // OK, string 有 length
logLength([1,2]); // OK
logLength(123); // 报错,number 没有 length
原理 :
T extends Lengthwise告诉编译器,T必须满足Lengthwise类型(具有length: number)。这样函数体内就可以安全访问arg.length。
泛型接口与泛型类
typescript
interface Result<T> {
data: T;
success: boolean;
}
const res: Result<string> = { data: "ok", success: true };
class Stack<T> {
private items: T[] = [];
push(item: T) { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
}
const numberStack = new Stack<number>();
numberStack.push(10);
注意:泛型在类中为实例方法提供类型安全,静态成员不能使用类的泛型。
八、高级类型:构建类型的强大工具
8.1、索引类型查询 keyof
概念
keyof T 取出类型 T 的所有键名 ,生成一个联合类型。
为什么需要?
当你想限制某个变量只能是对象的合法键名时,而不是任意的 string,就可以用 keyof。
示例
typescript
interface Person {
name: string;
age: number;
}
type PersonKeys = keyof Person; // "name" | "age"
使用场景
typescript
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person: Person = { name: "Alice", age: 25 };
const n = getProperty(person, "name"); // n 是 string
getProperty(person, "email"); // ❌ 报错,email 不是 keyof Person
原理与注意
keyof对对象类型返回其所有公共属性名的联合。- 对联合类型,
keyof返回所有联合成员共有的属性名(交集)。 - 对
any,keyof any是string | number | symbol。
8.2、索引访问类型 T[K]
概念
像访问对象属性一样访问类型,T[K] 得到类型 T 中属性 K 的类型。
示例
typescript
type PersonName = Person["name"]; // string
type PersonAge = Person["age"]; // number
结合 keyof
typescript
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
这里 T[K] 表示:返回值类型就是对象中该属性对应的类型。调用 getProperty(person, "name") 返回类型自动为 string。
注意
K必须满足extends keyof T,否则报错。- 可以通过联合类型一次性获取多种属性的类型:
Person["name" | "age"]得到string | number。
8.3、映射类型(Mapped Types)
概念
基于已有类型的键,通过遍历键来生成一个新类型,并可以给每个属性加上修饰符(readonly、?)或改变值的类型。
为什么需要?
比如你有一个 Todo 接口,想生成一个所有字段都可选的版本,或者所有字段只读的版本。手工重复定义非常繁琐且容易遗漏。
基础语法
typescript
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Partial<T> = {
[K in keyof T]?: T[K];
};
[K in keyof T]:遍历T的所有键,K在每次迭代中是一个键名字面量。- 右侧
T[K]:取出该键对应的类型。
示例
typescript
interface Todo {
title: string;
description: string;
completed: boolean;
}
type ReadonlyTodo = Readonly<Todo>;
// { readonly title: string; readonly description: string; readonly completed: boolean; }
type PartialTodo = Partial<Todo>;
// { title?: string; description?: string; completed?: boolean; }
更进一步:带条件的映射
你可以使用 as 子句对键进行重新映射(TypeScript 4.1+):
typescript
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User {
name: string;
age: number;
}
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number; }
这里 as 重命名了键,Capitalize 是内置工具类型,将首字母大写。
原理与注意
- 映射类型生成新的对象类型,不会影响原类型。
- 可以组合多个修饰符,例如
-readonly移除只读,-?移除可选。 - 遍历的是
keyof T,所以只处理对象类型的属性。
8.4、条件类型(Conditional Types)
概念
根据某个类型是否满足条件,返回不同的类型。语法类似于三元表达式:T extends U ? X : Y。
为什么需要?
很多工具类型需要根据输入类型的不同输出不同结果,比如提取函数返回类型、排除 null 等。
基础示例
typescript
type IsString<T> = T extends string ? true : false;
type A = IsString<"abc">; // true
type B = IsString<42>; // false
分布式条件类型
当条件类型的左侧是裸类型参数 (直接写成 T extends ...)且 T 是联合类型时,条件类型会自动分布到联合类型的每个成员上。
typescript
type ToArray<T> = T extends any ? T[] : never;
type StrOrNumArray = ToArray<string | number>; // string[] | number[]
// 而不是 (string | number)[]
这个特性是实现 Exclude、Extract 等工具类型的基础。
注意
- 要避免分布,可以用元组包裹:
[T] extends [U] ? ...。 - 条件类型中可以使用
infer进行类型推断。
8.5、infer 关键字
概念
infer 只能在条件类型的 extends 子句中使用,用于声明一个待推断的类型变量。
为什么需要?
当你需要从一个复杂类型中提取出某个部分时(比如函数返回值类型、Promise 包裹的类型),infer 可以帮你"解包"。
示例:提取函数返回类型
typescript
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = (x: number) => string;
type R = ReturnType<Fn>; // string
解释:
T extends (...args: any[]) => infer R:如果T是一个函数类型,那么推断出它的返回值类型并赋值给R。- 条件为真时返回
R,否则返回never。
更多应用
- 提取数组元素类型:
T extends (infer U)[] ? U : never - 提取 Promise 结果类型:
T extends Promise<infer V> ? V : never - 提取构造函数实例类型:
T extends new (...args: any[]) => infer I ? I : never
原理与注意
infer只能在extends的true分支中使用。- 同一个条件类型可以同时有多个
infer,例如提取函数参数类型:T extends (...args: infer P) => any ? P : never。
8.6、常用的内置工具类型
TypeScript 内置了许多基于上述特性实现的工具类型,掌握它们可以大幅提升开发效率。
1. Partial<T> ------ 所有属性变为可选
作用 :把类型 T 的所有属性都变成可选的(加上 ?)。
为什么需要:当你需要更新一个对象的某些字段时,可以传递部分属性而不必提供全部。
示例:
typescript
interface Todo {
title: string;
description: string;
completed: boolean;
}
type PartialTodo = Partial<Todo>;
输出结果类型:
typescript
{
title?: string;
description?: string;
completed?: boolean;
}
使用效果:
typescript
const todo1: PartialTodo = { title: "Learn TS" }; // ✅ 只传 title
const todo2: PartialTodo = {}; // ✅ 空对象也行
const todo3: Todo = { title: "Learn TS" }; // ❌ 缺少其他属性
解释 :Partial 内部遍历 T 的所有键,给每个属性前加上 ?,相当于把"必须填"变成了"选填"。
2. Required<T> ------ 所有属性变为必选
作用 :把类型 T 的所有属性都变成必填的(去掉 ?)。
为什么需要:有时从一个可选类型出发,需要生成一个所有字段都强制存在的类型。
示例:
typescript
interface Config {
host?: string;
port?: number;
debug?: boolean;
}
type RequiredConfig = Required<Config>;
输出结果类型:
typescript
{
host: string;
port: number;
debug: boolean;
}
使用效果:
typescript
const cfg: RequiredConfig = {
host: "localhost",
port: 8080,
debug: false
}; // ✅ 必须三个全给
const cfg2: RequiredConfig = { host: "localhost" }; // ❌ 缺少 port 和 debug
解释 :Required 内部使用了 -? 修饰符,意思是"移除可选",强制每个属性都必填。
3. Readonly<T> ------ 所有属性变为只读
作用 :把类型 T 的所有属性都变成只读的(加上 readonly)。
为什么需要:保证对象一旦创建就不能被修改,常用于配置对象或状态快照。
示例:
typescript
interface User {
name: string;
age: number;
}
type ReadonlyUser = Readonly<User>;
输出结果类型:
typescript
{
readonly name: string;
readonly age: number;
}
使用效果:
typescript
const user: ReadonlyUser = { name: "Alice", age: 25 };
user.name = "Bob"; // ❌ 报错:无法分配到 "name",因为它是只读属性
解释 :Readonly 内部遍历所有键,给每个属性前加上 readonly,防止修改。
4. Pick<T, K> ------ 从 T 中挑选指定属性
作用 :从类型 T 中只挑选出 K 中指定的属性,构造一个新类型。
为什么需要:当你只需要一个对象中的少数几个字段时,比如展示预览信息。
示例:
typescript
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
输出结果类型:
typescript
{
title: string;
completed: boolean;
}
使用效果:
typescript
const preview: TodoPreview = {
title: "Learn TS",
completed: false
}; // ✅ 只含两个属性
const preview2: TodoPreview = { title: "TS" }; // ❌ 缺少 completed
解释 :Pick 第一个参数是源类型,第二个参数是要保留的键的联合类型。内部遍历 K,取 T[K] 作为值类型。
5. Omit<T, K> ------ 从 T 中删除指定属性
作用 :从类型 T 中剔除 K 中指定的属性,返回剩下的。
为什么需要 :与 Pick 相反,当你知道不需要哪些字段时,用 Omit 更直观。
示例:
typescript
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoWithoutDesc = Omit<Todo, "description">;
输出结果类型:
typescript
{
title: string;
completed: boolean;
}
使用效果:
typescript
const todo: TodoWithoutDesc = {
title: "Learn TS",
completed: true
}; // ✅ 不用写 description
const todo2: TodoWithoutDesc = { title: "TS" }; // ❌ 缺少 completed
解释 :Omit 内部先通过 Exclude<keyof T, K> 得到剩余的键,再配合 Pick 生成新类型。本质上相当于 Pick<T, Exclude<keyof T, K>>。
6. Record<K, T> ------ 构造对象类型
作用 :创建一个对象类型,它的键都是 K 类型,值都是 T 类型。
为什么需要:快速定义字典、映射表、配置集等场景。
示例:
typescript
type Page = "home" | "about" | "contact";
type PageInfo = Record<Page, { title: string }>;
输出结果类型:
typescript
{
home: { title: string };
about: { title: string };
contact: { title: string };
}
使用效果:
typescript
const pages: PageInfo = {
home: { title: "Home" },
about: { title: "About" },
contact: { title: "Contact" }
}; // ✅ 键必须全部包含
const pages2: PageInfo = { home: { title: "A" } }; // ❌ 缺少 about 和 contact
解释 :Record 第一个参数 K 必须能赋值给 string | number | symbol,第二个参数 T 是值的类型。内部遍历 K 的每个成员,构造一个属性,所有值类型都是 T。
7. Exclude<T, U> ------ 从联合类型中排除
作用 :从联合类型 T 中剔除所有能赋值给 U 的成员。
为什么需要:去除联合类型中的某些特定类型。
示例:
typescript
type Mixed = string | number | boolean;
type OnlyPrimitive = Exclude<Mixed, boolean>;
输出结果类型:
typescript
string | number
使用效果:
typescript
let x: OnlyPrimitive;
x = "hello"; // ✅
x = 42; // ✅
x = true; // ❌ boolean 已被排除
解释 :Exclude 利用了条件类型的分布式 特性,对 T 的每个成员执行 T extends U ? never : T,如果该成员能赋值给 U,就返回 never(从结果中消失),否则保留。
8. Extract<T, U> ------ 从联合类型中提取
作用 :从联合类型 T 中提取出所有能赋值给 U 的成员。
为什么需要 :与 Exclude 相反,获取交集部分。
示例:
typescript
type Mixed = string | number | boolean;
type StringOrBool = Extract<Mixed, string | boolean>;
输出结果类型:
typescript
string | boolean
使用效果:
typescript
let y: StringOrBool;
y = "abc"; // ✅
y = false; // ✅
// y = 10; // ❌ number 不在提取范围内
解释 :内部实现是 T extends U ? T : never,只保留能赋值给 U 的成员。
9. NonNullable<T> ------ 排除 null 和 undefined
作用 :从类型 T 中剔除 null 和 undefined。
为什么需要:确保一个值不为空,常用于严格模式下。
示例:
typescript
type MaybeString = string | null | undefined;
type SureString = NonNullable<MaybeString>;
输出结果类型:
typescript
string
使用效果:
typescript
let s: SureString;
s = "hello"; // ✅
s = null; // ❌
s = undefined; // ❌
解释 :底层实现 T extends null | undefined ? never : T,把空值过滤掉。也可以写成 T & {} 达到相似效果。
10. ReturnType<T> ------ 获取函数返回值类型
作用:提取一个函数类型的返回值类型。
为什么需要:当你需要复用某个函数的返回值类型,或者做依赖推断时。
示例:
typescript
function createUser(name: string, age: number) {
return { name, age, id: Math.random() };
}
type UserReturn = ReturnType<typeof createUser>;
输出结果类型:
typescript
{
name: string;
age: number;
id: number;
}
使用效果:
typescript
const user: UserReturn = {
name: "Alice",
age: 25,
id: 0.12345
}; // ✅ 结构与函数返回值一致
const user2: UserReturn = { name: "Bob" }; // ❌ 缺少 age 和 id
解释 :ReturnType 内部用 infer 推断返回值类型。typeof createUser 获取函数类型,然后 extends (...args: any[]) => infer R ? R : any 提取 R。
11. Parameters<T> ------ 获取函数参数类型
作用:提取一个函数类型的参数列表类型,以元组形式返回。
为什么需要:需要复用某个函数的参数列表,或做参数转发。
示例:
typescript
function createUser(name: string, age: number) {
return { name, age };
}
type CreateUserParams = Parameters<typeof createUser>;
输出结果类型:
typescript
[name: string, age: number]
使用效果:
typescript
const args: CreateUserParams = ["Alice", 25]; // ✅ 按参数顺序和类型提供
const args2: CreateUserParams = [123, "Bob"]; // ❌ 类型顺序不对
解释 :内部用 infer 提取:T extends (...args: infer P) => any ? P : never。
12. Awaited<T> ------ 递归解包 Promise
作用 :获取 Promise 最终 resolve 的类型,支持嵌套 Promise。
为什么需要:异步函数经常包装多层 Promise,想获得最终结果类型。
示例:
typescript
type X = Promise<string>;
type Y = Promise<Promise<number>>;
type Z = Promise<Promise<Promise<boolean>>>;
type AX = Awaited<X>; // string
type AY = Awaited<Y>; // number
type AZ = Awaited<Z>; // boolean
输出结果类型:
typescript
string, number, boolean
使用效果:
typescript
async function fetchData(): Promise<string> {
return "data";
}
type Data = Awaited<ReturnType<typeof fetchData>>; // string
// 直接得到 string,而不是 Promise<string>
解释 :Awaited 内部递归:T extends Promise<infer V> ? Awaited<V> : T,直到剥到非 Promise 类型为止。
综合应用举例
实际项目中,常常组合使用这些工具类型:
typescript
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
interface User {
id: number;
name: string;
email: string;
password: string;
}
// 返回给前端的用户信息,去掉 password
type PublicUser = Omit<User, "password">;
// 前端请求到的响应体数据类型
type UserResponse = ApiResponse<PublicUser>;
// 结果:{ data: { id: number; name: string; email: string }; status: number; message: string; }
// 更新用户时只传部分字段
type UpdateUserPayload = Partial<Pick<User, "name" | "email">>;
// 结果:{ name?: string; email?: string; }
8.7、高级类型组合实例:DeepReadonly
利用映射类型、条件类型和递归,实现深层只读:
typescript
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K];
};
interface Config {
db: {
host: string;
port: number;
};
debug: boolean;
}
type ReadonlyConfig = DeepReadonly<Config>;
// { readonly db: { readonly host: string; readonly port: number }; readonly debug: boolean; }
这个例子综合了 keyof、映射类型、条件类型和递归,足以体现高级类型的强大。
九、类(Class)在 TypeScript 中的增强
9.1、类的基本写法(类型注解)
TypeScript 允许在类中为属性添加类型注解,构造函数参数也可以标注类型。
typescript
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const p = new Point(10, 20);
p.x = 30; // ✅
p.x = "hello"; // ❌ 类型错误
解释 :属性 x 和 y 的类型都是 number,任何赋值都会检查类型。如果没有显式声明属性,直接在 constructor 中 this.x = x 会报错,因为 TS 要求先声明属性。
9.2、访问修饰符 ------ 控制属性和方法的可见性
访问修饰符决定了属性或方法可以在哪里被访问。TypeScript 提供了三个修饰符:
public(默认):任何地方都能访问。protected:当前类和子类能访问,类外部不能访问。private:只有当前类自己能访问,子类和外部都不能访问。
9.2.1. public 显式或默认
typescript
class Animal {
public name: string; // 等价于不写修饰符
constructor(name: string) {
this.name = name;
}
}
const cat = new Animal("Kitty");
console.log(cat.name); // ✅ 外部可读
cat.name = "Tom"; // ✅ 外部可写
解释 :不加修饰符就是 public,外部可随意访问。
9.2.2. protected ------ 允许子类访问
typescript
class Animal {
protected age: number;
constructor(age: number) {
this.age = age;
}
protected getAge(): number {
// ✅ 内部可以访问 protected 属性
return this.age;
}
}
class Dog extends Animal {
constructor(age: number) {
super(age);
}
public showAge() {
console.log(this.age); // ✅ 子类可以访问 protected 属性
console.log(this.getAge()); // ✅ 子类可以调用 protected 方法
}
}
const dog = new Dog(3);
dog.showAge(); // 3
console.log(dog.age); // ❌ 错误:属性"age"是受保护的,只能在类及子类中访问
dog.getAge(); // ❌ 错误
解释 :age 和 getAge() 都是 protected,只能在 Animal 内部及其子类 Dog 中访问,实例 dog 外部不能访问。这样的封装可以对外隐藏内部细节,又能让派生类继承。
9.2.3. private ------ 只有自身能访问
typescript
class Animal {
private secret: string;
constructor(secret: string) {
this.secret = secret;
}
public reveal() {
return this.secret; // ✅ 类内部可以访问
}
}
class Cat extends Animal {
constructor() {
super("cat secret");
}
public tryReveal() {
return this.secret; // ❌ 错误:属性"secret"为私有属性,只能在类"Animal"中访问
}
}
const animal = new Animal("my secret");
console.log(animal.reveal()); // ✅
console.log(animal.secret); // ❌ 错误
解释 :private 只有当前类内部能访问,子类也不行。这用来隐藏真正的内部实现。
9.2.4. readonly ------ 只读属性
typescript
class Person {
readonly id: number;
public name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
const p = new Person(1, "Alice");
console.log(p.id); // ✅
p.id = 2; // ❌ 错误:无法分配到 "id",因为它是只读属性
p.name = "Bob"; // ✅ 非只读属性可以修改
解释 :readonly 属性只能在声明时或构造函数中赋值,之后不能修改。适合用作 ID、创建时间等不可变数据。
9.2.5. 参数属性 ------ 简写形式
在构造函数参数前直接加上访问修饰符或 readonly,可以同时声明属性和赋值。
typescript
class Employee {
constructor(
public name: string,
private salary: number,
readonly id: number
) {
// 不需要手动赋值,TS 会自动完成
}
}
const emp = new Employee("Bob", 5000, 101);
console.log(emp.name); // ✅
console.log(emp.salary); // ❌ 私有属性
emp.id = 102; // ❌ 只读
解释 :public name 相当于在类中声明了 public name: string,然后在构造函数中自动把参数值赋给 this.name。这大大减少了样板代码。
9.3、抽象类 ------ 不能被实例化的类
概念与为什么需要
当你有一个基类,它本身不应该被直接 new 出来,而应该由子类去实现具体逻辑时,就用抽象类。抽象类只描述"应该有什么",不负责具体实现。
- 用
abstract关键字标记类或方法。 - 抽象方法只有签名,没有方法体,子类必须实现。
- 可以包含具体实现的方法(非抽象方法)。
示例
typescript
abstract class Shape {
abstract getArea(): number; // 抽象方法:子类必须实现
// 具体方法:子类可以直接用
describe(): void {
console.log("This shape's area is " + this.getArea());
}
}
class Circle extends Shape {
constructor(public radius: number) {
super(); // 必须调用基类构造函数
}
// 实现抽象方法
getArea(): number {
return Math.PI * this.radius ** 2;
}
}
const shape = new Shape(); // ❌ 错误:不能创建抽象类的实例
const circle = new Circle(10);
circle.describe(); // This shape's area is 314.159...
console.log(circle.getArea()); // 314.159...
正确与错误:
typescript
// ✅ 子类继承抽象类,实现了 getArea
class Rectangle extends Shape {
constructor(public width: number, public height: number) {
super();
}
getArea(): number {
return this.width * this.height;
}
}
// ❌ 没有实现抽象方法 getArea,会报错
class Square extends Shape {
constructor(public side: number) {
super();
}
// 缺少 getArea
}
解释:抽象类强制子类遵循约定的接口,让设计更清晰。
9.4、类实现接口 (implements)
概念与为什么需要
有时你想让一个类必须 包含某些属性和方法,但不需要从某个父类继承实现。这时可以用接口 定义契约,用 implements 来保证类符合接口。
示例
typescript
interface Printable {
content: string;
print(): void;
}
class Document implements Printable {
content: string;
constructor(content: string) {
this.content = content;
}
print(): void {
console.log(this.content);
}
}
const doc = new Document("Hello TypeScript");
doc.print(); // Hello TypeScript
错误示例:
typescript
// ❌ 缺少 content 属性或 print 方法
class Report implements Printable {
print() { console.log("empty"); }
}
// 报错:类"Report"缺少属性"content"
解释 :implements 只在类型层面进行检查,编译后不产生任何代码。一个类可以实现多个接口,用逗号分隔。
9.5、类既是值又是类型
在 TypeScript 中,class 声明既创建了运行时值 (构造函数),也创建了类型(实例的类型)。
typescript
class Cat {
name: string;
constructor(name: string) {
this.name = name;
}
}
// 作为类型使用
let cat: Cat; // Cat 是实例的类型
cat = new Cat("Kitty"); // ✅
cat = { name: "Tom" }; // ✅ 结构类型兼容
// 作为值使用(构造函数本身)
const CatCtor: typeof Cat = Cat;
const anotherCat = new CatCtor("Jerry");
解释 :当你写 let c: Cat 时,Cat 是实例的类型;当你写 typeof Cat 时,拿到的是构造函数本身的类型。这种双重性质在依赖注入、工厂函数中很有用。
9.6、重要注意事项
9.6.1. TypeScript 的 private 和 protected 只在编译时检查
编译成 JavaScript 后,这些修饰符会被移除,属性在运行时仍然是可访问的。
typescript
class Secret {
private data = "hidden";
}
const s = new Secret();
(s as any).data; // 运行时可访问,但 TS 编译器会报错
真正的运行时私有 :使用 ECMAScript 的 # 私有字段(硬性私有)。
typescript
class BankAccount {
#balance = 0; // JavaScript 原生私有字段
getBalance() {
return this.#balance;
}
}
const account = new BankAccount();
account.#balance; // ❌ 语法错误(运行时也是私有的)
9.6.2. 构造函数参数用 public 简写时,注意与属性同名覆盖
typescript
class Foo {
constructor(public x: number) {}
}
// 等价于:
class Foo {
x: number;
constructor(x: number) {
this.x = x;
}
}
9.6.3. 抽象类可以被继承但不能 new,而接口只能被 implements 或继承,不能实例化。
9.7、总结
| 特性 | 作用 | 关键字 |
|---|---|---|
| 访问修饰符 | 控制可见性 | public, protected, private |
| 只读属性 | 禁止修改 | readonly |
| 参数属性 | 声明并赋值一步到位 | 构造函数的修饰符参数 |
| 抽象类 | 强制子类实现特定方法 | abstract class |
| 抽象方法 | 无实现,由子类提供 | abstract method() |
| 接口实现 | 保证类满足接口契约 | implements |
十、模块与命名空间
现代项目使用 ES Modules(import / export)。
typescript
// point.ts
export interface Point { x: number; y: number; }
export function draw(p: Point) { ... }
// app.ts
import { Point, draw } from "./point";
namespace 主要是早期用于组织全局代码的,现在已被 ES Modules 取代。了解即可,不推荐新项目使用。
十一、类型声明文件(.d.ts)
当你在 TypeScript 项目中使用 JavaScript 库时,TS 无法得知库的类型,需要声明文件来描述类型。
- 社区大多已提供:
npm install @types/库名 - 如果没有,可自行编写
xxx.d.ts:
typescript
// global.d.ts
declare module "*.css" {
const content: { [className: string]: string };
export default content;
}
// 声明全局变量或方法
declare global {
interface Window {
myApp: any;
}
}
注意:声明文件只用于类型检查,不包含实现。
十二、tsconfig.json 关键配置
tsconfig.json 是 TypeScript 项目的配置文件,控制编译行为。
json
{
"compilerOptions": {
"target": "ES2020", // 编译后的 JS 版本
"module": "ESNext", // 模块系统
"lib": ["ES2020", "DOM"], // 使用的内置 API 声明
"strict": true, // 启用所有严格类型检查
"esModuleInterop": true, // 兼容 CommonJS 导入
"skipLibCheck": true, // 跳过声明文件检查,加速编译
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx"
},
"include": ["src"]
}
最重要的选项是 strict: true,它一次性开启以下严格检查:
noImplicitAny:禁止隐式 anystrictNullChecks:严格空值检查strictFunctionTypes:严格函数类型检查- 等等
初学者务必开启 strict,虽然一开始会多出很多报错,但这能帮你养成类型安全的习惯。
十三、面试常考实战题型
1. interface vs type(见第三节)
2. any vs unknown
unknown 是类型安全的顶类型,使用前必须检查类型。面试中经常要求说出两者区别并举例。
3. 泛型约束的应用
例如要求泛型参数必须具有某属性,或者根据泛型返回不同类型。
4. 提取数组元素类型
typescript
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type El = ArrayElement<string[]>; // string
5. 让对象所有属性可选 / 必选 / 只读
使用 Partial<T>, Required<T>, Readonly<T>。
6. as const 断言
将对象或数组变为字面量只读类型。
typescript
const colors = ["red", "green"] as const;
// 类型:readonly ["red", "green"]
作用:获得更精确的类型,且数据不可修改。
7. 双重断言
typescript
const el = document.getElementById("app") as unknown as HTMLCanvasElement;
不推荐,除非万不得已,一般先用类型守卫。
8. 编写自定义类型守卫
typescript
function isString(value: unknown): value is string {
return typeof value === "string";
}