TypeScript 核心概念完全指南
TypeScript 是由微软开发的一种开源的编程语言,是 JavaScript 的一个超集,扩展了 JavaScript 的语法。
从本质上说,TypeScript 是带有静态类型系统的 JavaScript,可以编译成普通的 JavaScript 代码。官网英文介绍里的 strongly typed,指的是编译期有一套完整的静态类型系统与工具链;并不等于运行时禁止隐式类型转换(那是另一维度的「强/弱类型」划分,见后文)。
TypeScript is JavaScript with syntax for types.
TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale.
JavaScript 的缺陷
JavaScript(实现 ECMAScript 标准)诞生时是一种简单的浏览器脚本语言。当它被发明时,它被期望用于在网页中嵌入简短的代码片段。这些片段通常不超过几十行。因此,早期的 Web 浏览器执行此类代码的速度非常慢。但是,随着时间的推移,JS 变得越来越流行,并且 Web 开发者开始使用它来创造交互式体验。
JavaScript 的先天缺陷:
- 缺少静态类型约束:JS 类型仅在运行时校验,类型错误要等到代码执行才暴露,线上容易出现隐性 BUG。
- 代码提示功能弱:无类型信息,编辑器难以提供精准智能提示、参数推断。
- 大规模项目维护困难:缺少类型注解,函数入参、返回值、对象结构无明确定义,代码可读性差。
有哪些特性?
TypeScript 的核心目标:给动态弱类型的 JavaScript 增加静态类型系统,在编译期捕获错误。
类型系统(最核心)
从 TypeScript 的名字就可以看出来,「类型」是其最核心的特性。
JavaScript 是一门非常灵活的编程语言:
- 没有类型约束:一个变量可能初始化时是字符串,过一会儿又被赋值为数字。
- 隐式类型转换:变量的类型很难在运行前就确定。
- 基于原型的面向对象编程:原型上的属性或方法可以在运行时被修改。
TypeScript 的类型系统,为 JavaScript 提供了静态类型检查以及使用看起来像基于类的面向对象编程语法操作 Prototype,并且与编辑器紧密集成,在编辑器中尽早捕获错误,在很大程度上弥补了 JavaScript 的缺点。
静态类型
动态类型是指在运行时才会进行类型检查,这种语言的类型错误往往导致运行时错误。JavaScript 没有独立的静态类型检查阶段,类型要到运行时才确定,是动态类型。
静态类型是指在编译阶段就能确定每个变量的类型,这种语言的类型错误会在编译期报错。TypeScript 在运行前需要先编译为 JavaScript,而在编译阶段就会进行类型检查,所以 TypeScript 是静态类型。
let foo = 1;
foo.split(' ');
// JavaScript 运行时会报错:Uncaught TypeError: foo.split is not a function
// TypeScript 编译时会报错:Property 'split' does not exist on type 'number'.
弱类型
类型系统按照「是否允许隐式类型转换」分为强类型和弱类型。
JavaScript 和 TypeScript 都是弱类型语言,所以以下代码都能正常运行:
TypeScript 是完全兼容 JavaScript 的,它不会修改 JavaScript 运行时的特性,所以它也是弱类型。
let a = 1 + '1';
a; // "11"
typeof a; // "string"
而强类型,比如 Python,以下代码在运行时报错:
print(1 + '1')
# TypeError: unsupported operand type(s) for +: 'int' and 'str'
强弱是相对的,Python 在处理整型和浮点型相加时,会将整型隐式转换为浮点型,但是这并不影响 Python 是强类型的结论,因为大部分情况下 Python 并不会进行隐式类型转换。
而 TypeScript 虽然不限制加号两侧的类型,但可以借助 TypeScript 提供的类型系统,以及 ESLint 提供的代码检查功能,来限制加号两侧必须同为数字或同为字符串。这在一定程度上使得 TypeScript 向「强类型」更近一步了——当然,这种限制是可选的。
TypeScript 的核心设计理念:在完整保留 JavaScript 运行时行为的基础上,通过引入静态类型系统来提高代码的可维护性,减少可能出现的 bug。
为什么要使用 TypeScript?
Rollbar 于 2018 年统计了前端项目中 Top10 的错误类型:
Uncaught TypeError: Cannot read property
TypeError: ‘undefined’ is not an object (evaluating
TypeError: null is not an object
(unknown): Script error
TypeError: Object doesn’t support property
TypeError: ‘undefined’ is not a function
Uncaught RangeError
TypeError: Cannot read property ‘length’
Uncaught TypeError: Cannot set property
ReferenceError: event is not defined
这其中很多是 null 或未定义的错误,而一个好的静态类型检查系统可以帮助避免它们。
TypeScript 的优势:
- 编译期捕获大量类型错误:在代码编译阶段校验类型、参数、对象属性等问题。
- 增强了编辑器和 IDE 的功能:支持代码实时补全、接口字段悬浮提示、一键跳转到定义、全局安全重构等能力。
- 提高代码质量和可维护性:通过类型注解、接口、类型别名明确数据结构、函数入参和返回值,类型定义即标准化代码文档。
- 接入成本可控:完全兼容原生 JavaScript 语法,渐进式接入、零侵入改造,兼容主流前端、Node 框架。
基础示例
安装:npm install -g typescript
运行:tsc hello.ts、tsc -w hello.ts(启动监听模式)
约定使用 TypeScript 编写的文件以 .ts 为后缀,用 TypeScript 编写 .jsx 时,以 .tsx 为后缀。
// hello.ts
function sayHello(person: string) {
return 'Hello, ' + person;
}
const user = 'Tom';
console.log(sayHello(user));
生成一个编译好的文件 hello.js:
function sayHello(person) {
return 'Hello, ' + person;
}
var user = 'Tom';
console.log(sayHello(user));
原始类型
TypeScript 的原始类型完全对标 JavaScript 原始数据类型,一共 7 种:
stringnumberbooleannullundefinedsymbolbigint
let userName: string = 'Tom';
let title: string = `This is ${userName} sawyer`;
let decimalismNum: number = 6; // 十进制数字
let hexNum: number = 0xf00d; // 十六进制数字
let binaryNum: number = 0b1010; // 二进制数字
let octonaryNum: number = 0o744; // 八进制数字
let notANum: number = NaN;
let infinityNum: number = Infinity;
let b1: boolean = false;
let b2: boolean = Boolean(true);
let undef: undefined = undefined;
let nul: null = null;
let sym1: symbol = Symbol("key");
let sym2: symbol = Symbol("key");
let big: bigint = 999999999999999999999999n;
注意:未开启 strictNullChecks 时,undefined 和 null 可赋值给数字、字符串等几乎所有类型。开启后(strict 默认包含此项,现代项目一般都会开),它们只能赋值给 any、unknown、自身、包含它们的联合类型,以及(对 undefined)void。
JavaScript 内置 3 个包装构造函数:String、Number、Boolean。它们构造出的是对象,类型应标注为 String、Number、Boolean,不要用原始类型 string、number、boolean 去标注(日常开发也几乎不需要这些包装对象)。
const s2: String = new String("test");
const n2: Number = new Number(123);
const b2: Boolean = new Boolean(true);
字面量类型
字面量类型是原始基础类型的子类型,限定变量只能为某个固定的值。
字面量类型单独使用意义不大,联合字面量是最常用写法,用来限定一组可选常量。
4 类字面量类型:
- 字符串字面量类型
- 数字字面量类型
- 布尔字面量类型
- bigint 字面量类型
let action: "add";
action = "add"; // 正确
action = "delete"; // 类型报错
type Action = "add" | "edit" | "delete";
function handleAction(op: Action) {
switch(op) {
case "add": break;
case "edit": break;
case "delete": break;
}
}
handleAction("query"); // 报错,不允许非法参数
type Code = 200 | 400 | 404;
let resCode: Code = 200;
resCode = 500; // 报错,不允许非法参数
type IsOk = true;
let flag: IsOk = true;
flag = false; // 报错,不允许非法参数
type BigId = 123n;
const id: BigId = 123n;
复合类型
复合类型是指拥有内部结构、由多个成员组成、编译后存在对应的 JavaScript 实体的类型。
复合类型清单:
- 对象类型
- 数组类型
- 元组
- 函数类型
- 枚举
- 类实例类型
- 内置容器对象类型
对象类型
对象类型描述一个对象拥有哪些属性、属性类型、可选、只读修饰,通常用来表示非原始基础类型。
let val: object;
val = { a: 1 };
val = [1,2,3];
val = () => {};
val = new Map();
val = 99; // 报错
val = null; // 报错(strictNullChecks 下)
注意:不推荐直接使用 object 类型。声明为 object 之后,不能访问任意属性、方法。
let data: object = { name: "lizhao" };
data.name; // 编译报错:Property 'name' does not exist on type 'object'
object 类型真正适合的场景极少:
function clone<T extends object>(target: T): T {
return { ...target };
}
function isObject(target: unknown): target is object {
return target !== null && typeof target === "object";
}
数组类型
数组类型存放同类型元素的有序集合,长度可变。
const nums: number[] = [1, 2, 3];
const strs: Array<string> = ["a", "b"];
const list: readonly number[] = [10, 20];
const arr: (string | number)[] = [1, "hello"];
元组
元组是固定长度、每个索引位置拥有独立类型的特殊数组。
type Tuple = [string, number];
type Row = [string, number?];
type List = [string, ...number[]];
const tuple: Tuple = ["身高", 180];
tuple[0] = "体重";
tuple[2] = 90; // 超出长度,报错
函数类型
函数类型描述函数的参数类型、返回值类型,约束可调用结构。
// 函数声明注解
function sum(a: number, b: number): number {
return a + b;
}
// 箭头函数类型表达式(类型别名复用)
type Handler = (value: string) => boolean;
const check: Handler = (s) => s.length > 0;
// 用接口定义函数
interface SumFunc {
(x: number, y?: number): number;
}
interface User {
id: number;
}
interface UserApi {
getUser: (id: number) => Promise<User>;
}
枚举
枚举定义一组命名常量,用于固定有限的选项集合。现代工程里也常用 as const 对象 + 字面量联合类型替代 enum,按团队约定选择即可。
// 数字枚举
// 默认从 0 开始,依次 +1,支持手动设置起始值或者全部指定
// 支持双向映射
enum Direction1 { Up, Down, Left, Right }
enum Direction2 { Up = 1, Down, Left, Right }
enum Direction3 { Up = 1, Down = 3, Left = 5, Right = 10 }
console.log(Direction1.Up); // 0
console.log(Direction1[0]); // "Up"
// 字符串枚举
enum Status {
Success = "SUCCESS",
Fail = "FAIL"
}
const s = Status.Success;
/*
编译结果:
var Status;
(function (Status) {
Status["Success"] = "SUCCESS";
Status["Fail"] = "FAIL";
})(Status || (Status = {}));
var s = Status.Success;
*/
// 常量枚举
const enum ConstStatus {
Success = "SUCCESS",
Fail = "FAIL"
}
const cs = ConstStatus.Success;
/- 编译结果:var cs = "SUCCESS"; */
备注:开启 isolatedModules 或用 Babel 转译时,不要使用 const enum,会拿不到枚举对象。
类实例类型
class 既是运行时构造函数,同时本身可以作为实例的类型。
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
// User 作为类型,约束必须是该类实例
const u: User = new User("Lizhao");
// 访问修饰符
// 仅用于 TypeScript 编译时代码的检查,并不影响运行时的可访问性
class User {
public name: string;
private age: number;
protected sex: string;
constructor(name: string, age: number, sex: string) {
this.name = name;
this.age = age;
this.sex = sex;
}
}
// 不能直接 new 实例,仅作为父类,定义抽象方法强制子类实现
abstract class Animal {
abstract run(): void;
}
内置容器对象类型
JavaScript 内置的引用容器,TypeScript 提供配套泛型类型,全部属于复合类型:Map<K,V>、Set<T>、Promise<T>、Date、RegExp。
const map: Map<string, number> = new Map();
const set: Set<string> = new Set();
function fetchData<T>(data: T): Promise<T> {
return Promise.resolve(data);
}
const now: Date = new Date();
const reg: RegExp = /^\d+$/;
特殊类型
特殊类型(特异类型)是既不属于「原始类型」、也不属于「复合类型」的类型,是 TS 内置顶层类型,用于特殊类型场景。
特殊类型有:
anyunknownvoidnever
any
any 是指任意类型。也就是,可以赋值给任意类型,任意类型也可以赋值给 any。
let data: any;
data = 1;
data = "abc";
data = {};
data.xxx();
data.aaa = 123;
变量声明时既未标注类型、也没有足够的初始值线索,在未开启 noImplicitAny 时会被识别为 any;开启 strict(含 noImplicitAny)后,这类写法会直接报错,需要显式标注或给出可推论的初始值:
let something; // 无 strict 时相当于 any;strict 下报错
something = 'seven';
something = 7;
let value: any; // 需要 any 时请显式写出
unknown
unknown 类型代表未知类型。
任意值都可以赋值给 unknown,但 unknown 不能直接赋值给 any、unknown 以外的类型,也不能直接访问属性、调用函数,必须先做类型缩小(类型判断、类型断言)。
let val: unknown;
val = 99;
val = "test";
let str: string = val; // 报错,不能直接赋值
if (typeof val === "string") { // 正常
const s: string = val;
}
void
void 类型代表函数没有有效返回值,主要用于函数返回类型。
function log(msg: string): void {
console.log(msg);
}
注意:变量声明为 void 没有什么用,因为仅能将它赋值为 undefined。
never
never 类型表示的是那些永不存在的值的类型。
两个典型场景:
- 抛出异常、无限循环、无法正常返回的函数返回类型
- 类型收窄后穷尽分支校验:理论上永远执行不到的分支。
// return 永远不会走到,没有正常完成执行
function error(msg: string): never {
throw new Error(msg);
}
// 函数永远执行不完,没有正常完成执行
function loop(): never {
while (true) {}
}
type Status = "success" | "fail";
function handleStatus(s: Status) {
switch (s) {
case "success":
console.log("成功");
break;
case "fail":
console.log("失败");
break;
default:
// 所有可能性被分支处理后,剩下的分支类型收敛为 never
const _check: never = s;
}
}
注意:如果新增类型:type Status = "success" | "fail" | "loading",此时 switch 缺少 loading 的 case,s 不再是 never,TS 直接编译报错。
注意:never 是所有类型的子类型,可以赋值给任何类型,但没有任何值(除 never 自身)可以赋值给 never。
interface
interface(接口)是 TS 提供的语法,用来描述对象的结构,规定对象应该拥有哪些属性、方法、属性的类型、可选、只读等约束。
interface 是一种声明类型的方式,不能直接定义原始类型、联合类型,不能给元组起别名。
interface 只存在编译阶段,编译为 JavaScript 后会被彻底移除,没有任何运行时代码。
interface 接口名 {
属性名: 类型;
方法名(参数): 返回类型;
}
interface User {
readonly id: number; // 只读属性
name: string;
avatar?: string; // 可选
hello(msg: string): void;
say: (msg: string) => void;
}
// 索引签名:约束「未知 key」的值类型
// 注意:所有已知属性的类型必须能赋值给索引签名的值类型
interface StringMap {
id: string;
[propName: string]: string;
}
interface LoginFn {
(account: string, pwd: string): boolean;
}
// 以下是非法
interface T = string | number
interface Point = [string, number]
interface ID = string
extends(继承)
extends 用于让一个接口复制另一个接口的成员,并可以追加新成员;覆盖已有成员时,新类型必须能赋值给原类型。支持单继承、多继承。
interface User {
id: number;
name: string;
}
interface Admin extends User {
roles: string[];
}
interface A { a: number }
interface B { b: string }
interface C extends A, B {
c: boolean
}
implements(实现)
implements 用于约束类的结构,确保类包含接口中声明的所有实例成员(属性 + 方法)。
interface A {
name: string;
}
interface B {
hello(): void;
}
class C implements A {
name = 'lizhao';
}
class D implements A, B {
name = 'lizhao';
hello() {
console.log("world");
}
}
声明合并
声明合并是指多个同名 interface 会自动合并所有成员。
注意:相同属性名,类型必须完全一致,否则报错;同名方法会形成函数重载。
interface User {
id: number;
}
interface User {
name: string;
}
// 自动合并,相当于
interface User {
id: number;
name: string;
}
type
type 用于创建类型别名,允许为任何类型(原始类型、联合类型、交叉类型、对象类型、函数类型等)起一个名字。
type 类型名称 = 类型定义;
type UserID = string | number;
type Color = 'red' | 'blue' | 'green';
type Callback = (data: any) => void;
type Point = { x: number; y: number };
泛型
泛型(Generics)是一种类型参数化的编程方式。它允许在定义函数、接口、类时不预先指定具体类型,而在使用时再传入类型。
一个函数可以支持多种类型的数据。
function identity<T>(value: T): T {
return value;
}
const result1 = identity<string>('hello');
const result2 = identity<number>(123);
const result3 = identity('world');
泛型函数使用方法:一是显式传入参数类型;二是利用类型推论——编译器会根据传入的参数自动确定类型。
// 调用时不传类型,则使用预先设置的默认类型
interface GenericResponse<T = any> {
data: T;
status: number;
}
const res1: GenericResponse = { data: 'anything', status: 200 };
const res2: GenericResponse<string> = { data: 'hello', status: 200 };
注意:T 是泛型参数名,也可以使用其他的泛型参数名(如 A、U、G),只要在数量和使用方式上能对应即可。
// 泛型函数:交换元组中两个元素
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
const swapped = swap([1, 'two']);
// 泛型接口
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
type UserResponse = ApiResponse<{ id: string; name: string }>;
type ProductResponse = ApiResponse<{ sku: string; price: number }>;
// 泛型类
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
泛型约束
使用泛型时,若需要限制泛型必须具有某些属性或方法,可以用 extends 约束。
function getLength<T>(arg: T): T {
console.log(arg.length);
return arg;
}
// Property 'length' does not exist on type 'T'.
interface HasLength {
length: number;
}
function getLength<T extends HasLength>(arg: T): T {
console.log(arg.length);
return arg;
}
getLength('hello');
getLength([1, 2, 3]);
getLength({ length: 10 });
getLength(123); // 报错,number 没有 length
内置工具
TypeScript 提供了一些内置泛型工具:
Partial<T>:将所有属性变为可选,Partial<{ a: number; b: number }> → { a?: number; b?: number }。Required<T>:将所有属性变为必选。Pick<T, K>:选取部分属性,Pick<User, 'id' | 'name'>。Omit<T, K>:排除部分属性,Omit<User, 'password'>。Record<K, T>:构造键值对类型,Record<'a' | 'b', number> → { a: number; b: number }。ReturnType<T>:传入一个函数类型T,提取出该函数的返回值类型,ReturnType<() => string> → string。
核心机制
TypeScript 核心机制:
- 类型推论:类型推断机制。在没有显式类型标注时,根据上下文自动推导类型。
- 类型断言:类型覆盖、类型转换机制。开发者手动告诉编译器某个值的具体类型,跳过编译器的自动推导。
- 类型收窄:控制流分析机制。通过代码逻辑分析,在特定代码块中将类型从宽泛缩窄到更具体。
类型推论
类型推论(Type Inference)是指 TypeScript 依据代码上下文自动推导类型,无需手写类型注解。无线索且未开启 noImplicitAny 时,推论为 any;开启 strict 后会直接报错。
- 变量赋值推论:初始化变量时,以初始值作为类型线索。没有初始值时,未开
noImplicitAny则推论为any。 - 函数返回值推论:函数不标注返回类型时,根据所有
return语句自动推论返回类型。 - 泛型调用的类型推论:根据传参,推论泛型的真实类型。
const userName = "Lizhao"; // 推论: "Lizhao"(const 字面量)
let title = "Lizhao"; // 推论: string
const user = { id: 1, name: 'Lizhao' }; // 推论: { id: number; name: string }
const list = [1, 2, 3]; // 推论: number[]
// 推论:number | string
function fn(x: boolean) {
if (x) return 100;
else return "text";
}
function identity<T>(arg: T): T {
return arg;
}
const s = identity("a"); // 推论:T = string
const n = identity(99); // 推论:T = number
类型断言
类型断言(Type Assertion)是开发者明确告诉 TypeScript:「我清楚这个变量是什么类型」。TypeScript 只检查断言是否足够相关(例如不能把 string 直接断言成 number),不会保证运行时一定成立;断言写错仍会造成运行时异常。
// 尖括号语法(JSX、TSX 文件禁止使用)
<类型>value
// as 语法(工程通用,推荐,兼容 .tsx)
value as 类型
const data: unknown = "hello world";
const l1 = (data as string).length;
const l2 = (<string>data).length;
类型断言使用场景:
unknown类型变量明确知道实际类型。- 常量断言(
as const)。 - DOM 获取元素,TypeScript 默认推断类型太宽泛。
- 联合类型缩小(慎用!优先用类型收窄)。
// unknown 类型
let res: unknown = JSON.parse('{"name":"test"}');
interface User {
name: string;
}
const user = res as User;
// as const:类型为字面量,"success"
const obj = {
type: "success" as const
};
// as const:类型为字面量元组,readonly ["red", "green", "blue"]
const colorsConst = ['red', 'green', 'blue'] as const;
// as const:类型为字面量类型,{ readonly url: "/api"; readonly method: "POST" }
const config = {
url: '/api',
method: 'POST'
} as const;
// TS 默认推论:Element | null,未必有 value 属性
const inputDom = document.querySelector("#input");
const input = document.querySelector("#input") as HTMLInputElement;
console.log(input.value);
// 联合类型缩小(更推荐用类型收窄,而不是断言)
type Status = string | number;
let s: Status = "success";
const msg = s as string;
类型收窄
类型收窄(Type Narrowing)是 TypeScript 根据运行时条件判断(typeof、in、instanceof、等值比较等),利用控制流分析,自动将宽泛类型缩小为更精确子类型;属于安全的类型缩小手段,应当优先使用,替代强制类型断言。
类型收窄依靠代码逻辑,TS 自动推导,安全;类型断言强制命令编译器,人为硬指定,存在风险。
原则:能使用类型收窄,绝不使用类型断言!
// 在不同分支自动缩小类型
function print(strOrNum: string | number) {
if (typeof strOrNum === "string") {
// 自动收窄为 string
console.log(strOrNum.toUpperCase());
} else {
// 自动收窄为 number
console.log(strOrNum.toFixed(2));
}
}
类型收窄的方法:
- typeof 类型守卫。
- 等值判断
===、!==。 in操作符:判断对象是否包含某个属性。instanceof:区分类实例。- 控制流分析:switch 分支、循环、return 提前终止。
高级类型
联合类型
联合类型(Union Type)表示值可以是多种类型其中任意一种。
TypeA | TypeB
type StrOrNum = string | number;
let num: StrOrNum = 'seven';
num = 7;
重要特性:在没有类型收窄之前,只能访问「所有类型共有的属性、方法」。
function getLength(x: string | number) {
x.toString(); // 正常:string、number 都拥有 toString
x.length; // 报错:number 没有 length,直接报错
}
交叉类型
交叉类型(Intersection Type)是把多个类型合并融合,新类型同时拥有所有类型的全部属性。多用于对象类型扩展。
interface User {
id: number;
name: string;
}
interface Permission {
roles: string[];
}
type Admin = User & Permission;
const admin: Admin = {
id: 1,
name: "Lizhao",
roles: ["admin"]
};
交叉类型特性:
- 融合多个对象类型,同时拥有所有属性;
原始类型相交常得到
never:一个值不可能同时满足多个原始类型。属性冲突时:多个类型存在同名属性,最终类型取两个属性类型的交叉。
type Wrong = string & number; // type Wrong = never
type A = { x: string };
type B = { x: number };
type C = A & B; // C = { x: never }
const c: C = { x: "abc" }; // 报错,不存在满足的值
关键字
keyof
接收对象类型,返回该类型所有属性名构成的字面量联合类型。
interface IObject {
a: string,
b: number,
c: boolean
}
type TKey = keyof IObject // type TKey = 'a' | 'b' | 'c'
// keyof string:得到 String 接口上的属性名联合(length、charAt、toUpperCase 等),并包含 number 索引
type A = keyof string;
// keyof any:任意对象可能的键类型
type B = keyof any; // string | number | symbol
typeof
返回一个变量、常量的类型。
const data = { id: 1, name: "tom" };
type Data = typeof data; // { id: number; name: string; }
const a = 'hello';
type T1 = typeof a; // 'hello'(const 字符串字面量会被收窄)
let b = 'hello';
type T2 = typeof b; // string
in
映射类型里用 in 遍历联合类型的每一个成员,构建新类型。运行时的 in 操作符则用于类型收窄,见上文。
type Keys = "a" | "b";
type Obj = {
[P in Keys]: boolean
}
// { a: boolean; b: boolean }
extends
3 种完全不同场景:
- 泛型约束:
T extends B,T必须能赋值给B。 - 条件类型:
A extends B ? T : F,A 是否能赋值给 B。 interface继承。
function log<T extends { length: number }>(val: T) {}
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
interface Base { id: number }
interface User extends Base { name: string }
注意:若类型为 X 的变量,可以安全赋值给标注类型 Y 的变量,则称 X 可赋值给 Y。
type HasLen = { length: number };
const s: string = "a";
const obj: HasLen = s;
infer
用于条件类型内部捕获推导子类型,只能写在 extends 子句的右侧。
infer R:让 TypeScript 自动推断占位位置的类型,存入变量 R。
type XXX<T> = T extends SomeType<infer R> ? R : never
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = () => string;
type Ret = MyReturnType<Fn>; // string
type ItemType<T> = T extends Array<infer Item> ? Item : never;
type T = ItemType<string[]>; // string
type UnPromise<T> = T extends Promise<infer V> ? V : T;
type U = UnPromise<Promise<number>>; // number
声明文件
声明文件是 TypeScript 中以 .d.ts 为后缀的文件,它只包含类型信息,不包含具体的实现代码。它的作用是描述 JavaScript 代码的类型结构,让 TypeScript 能够理解和使用那些用 JavaScript 编写的库或模块。
声明文件的获取方式:
- 内置声明文件:TypeScript 自带常见运行环境的类型声明(
node_modules/typescript/lib),比如:Boolean、Error、Date、RegExp、console、Document、HTMLElement、Event、NodeList、Promise 等。 - 库自带声明文件:许多现代库已经自带声明文件,会在
package.json中配置types或typings字段,指定声明文件的位置。 - npm 安装:
npm install --save-dev @types/xxx,TypeScript 会自动查找node_modules/@types/下的声明文件。 - 手动编写自定义声明文件。
核心环境
- 全局脚本(无
import、export):全局作用域,定义的类型直接挂载全局作用域,项目任意地方可直接使用。 - 模块文件(有
import、export):局部作用域,定义的类型默认仅在当前文件可用。
// 全局脚本
interface Window {
sdk: any;
}
declare const sdk: { start(): void };
// 模块文件
export {};
declare global {
interface Window {
sdk: any;
}
const sdk: { start(): void }; // ambient 上下文内不必再写 declare
}
命名空间
命名空间(namespace)是 TypeScript 提供的代码组织语法,将变量、函数、类、类型接口封装在独立作用域,隔离命名,减少全局污染。
现代工程优先使用 ESM 文件模块,命名空间(namespace)多用于类型声明(.d.ts)。
两个关键概念:
namespace:实体命名空间,可以写函数、变量等运行时代码,编译产生 JavaScript。可以嵌套命名空间。declare namespace:类型命名空间,仅用于类型描述,不能写运行时代码,编译不产生 JavaScript。
核心通用规则:
- 成员默认私有,必须添加
export导出才能对外暴露。 - 同名命名空间自动合并(声明合并)。
模块
Module(模块)是以文件为单元。TypeScript 的模块是基于 JavaScript 的模块标准(ESM、CommonJS)增加类型支持。
核心原则:
- 文件存在任意一条
import或export,就是模块;否则属于全局脚本,所有变量处于全局作用域。 - 内部成员默认私有,依靠
export对外暴露;
export {}
declare global {}
declare
declare 的核心作用是为运行时存在的 JavaScript 实体提供类型描述,让 TypeScript 能够理解和检查这些外部实体。它只做类型声明,不产生任何 JavaScript 运行时代码。
关键规则:
- 类型构造(
interface、type):永远不需要declare,编译时会直接删除,不会生成 JavaScript。 - 值声明(变量、函数、类、枚举):正常写会生成 JavaScript 代码。如果没有实现代码且只描述类型、不想产出 JavaScript,必须加
declare。 declare module:固定语法,只能写在顶层。写在全局脚本,完整重定义模块;写在模块文件,模块扩充,和原有模块类型合并。declare namespace:固定语法,可以写在顶层或者declare global内。declare global:固定语法,只能写在顶层,用于扩展全局类型。一般用于在模块文件,全局脚本不需要(天然属于全局作用域)。
注意:.d.ts 里写 declare interface 语法合法,但完全多余,行业不推荐。
注意:declare global 必须写在模块文件才有意义;写在全局脚本里多余,且可能报错。
declare 系列关键字清单:
declare var、declare let、declare const:声明全局变量类型。declare function:声明全局函数类型。declare class:声明全局类类型。declare enum: 声明全局枚举类型。declare namespace:创建命名空间(替代旧式模块)。declare module:模块类型声明。declare global:在模块文件开辟一块「全局作用域区域」,其内所有定义的类型会挂载到全局,参与全局声明合并。
声明语法
声明全局变量、全局函数(全局脚本模式):
declare const APP_VERSION: string;
declare function log(message: string): void;
interface Window {
track: (name: string) => void
}
扩展 Window、全局接口:
// 全局脚本
interface Window {
track: (name: string) => void
}
// 模块文件:必须使用 declare global
export {}
declare global {
interface Window {
track: (name: string) => void
}
}
模块声明:
// 全局脚本:重新定义模块
declare module "*.svg" {
const url: string;
export default url;
}
declare module "*.module.css" {
const classes: Record<string, string>;
export default classes;
}
// 模块文件:扩充模块
import "axios";
declare module "axios" {
interface AxiosRequestConfig {
skipLoading?: boolean;
}
}
export {};
注意:全局脚本直接写 declare module "axios",会覆盖全部原有类型,不是合并扩充!
命名空间声明:
declare namespace $SDK {
function init(): void;
const version: string;
}
三斜线指令
三斜线指令(Triple-Slash Directives)以 /// 开头、放在文件最顶部的单行注释指令,是 TypeScript 专属语法,仅在 .ts、.tsx、.d.ts 生效。
三斜线指令是包含单个 XML 标签的单行注释,用来加载声明文件。
/// <指令名 参数="xxx" />
四种常用三斜线指令:
- 用于声明对文件的依赖。
- 用于声明对包的依赖。
- 引入 TypeScript 内置标准库。
- 禁用 TypeScript 默认内置库。
/// <reference path="./other.d.ts" />
/// <reference types="node" />
/// <reference lib="dom" />
/// <reference no-default-lib="true"/>
默认情况下:TypeScript 编译器会加载命令行指定的,或者 tsconfig.json 里 files 字段指定的所有声明文件。如果需要加载不在指定范围的声明文件,就需要用到三斜线指令。
注意:模块文件中三斜线指令仍然有效(例如 /// <reference types="node" />),但行为更接近「声明依赖」而非把内容注入全局脚本作用域。若指定了 --noResolve,三斜线指令会被忽略。
注意:现代业务代码优先使用 import;三斜线指令主要用于独立 .d.ts 类型定义文件或需要显式拉取 @types、内置 lib 的场景。
声明合并
声明合并是 TypeScript 的一个独特特性:当多个同名的声明(接口、命名空间、类等)同时存在时,TypeScript 会将它们自动合并成一个单一的类型定义。
合并规则:
- 并非所有同名声明都能合并。例如
type别名与interface同名会冲突。 - 接口合并:同名接口的属性会合并,方法会形成重载列表,同名属性类型必须一致(否则报错)。
- 命名空间合并:仅导出成员合并,未导出成员相互隔离、不可跨块访问。
- 模块合并:模块文件里做模块扩充,与原有模块类型合并;全局脚本里
declare module会完整重定义模块,覆盖原有类型,不合并。 - class 与同名
interface、namespace可以合并;type别名、变量、函数不能重复同名定义。
export {}
declare module 'lodash' {
interface LoDashStatic {
customMethod(): void;
}
}
发布库声明文件
当发布一个 npm 包时,应在 package.json 中指定声明文件位置:
{
"main": "dist/index.js",
"types": "dist/index.d.ts",
"typings": "dist/index.d.ts"
}
types 为类型入口(优先级高于 typings);typings 用于兼容旧版工具。
tsconfig.json
tsconfig.json 是 TypeScript 项目的编译配置文件,位于项目根目录,告诉 TypeScript 编译器如何编译项目。
# 初始化配置文件
tsc --init
# 使用默认配置编译(自动查找 tsconfig.json)
tsc
# 指定配置文件路径
tsc -p ./path/to/tsconfig.json
tsc --project ./path/to/tsconfig.json
注意:如果 tsconfig.json 文件是个空文件,那么所有默认的文件都会以默认配置选项编译。
注意:在命令行上指定的编译选项会覆盖在 tsconfig.json 文件里的相应选项。
TypeScript 配置文件包含七个顶级配置项:
compilerOptions:核心编译选项。files:指定编译文件列表(精确匹配)。include:指定要编译的文件模式,支持 glob 通配符。exclude:指定要排除的文件模式,支持 glob 通配符。extends:继承另一个配置文件。references:项目引用(多项目构建)。watchOptions:监听模式配置。
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "bundler",
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}
compilerOptions 常见配置:
target:指定 ECMAScript 目标版本。可选值有:ES3、ES5、ES6(ES2015)、ES2016、ES2017、ES2018、ES2019、ES2020、ES2021、ES2022、ESNext。module:指定生成的模块代码格式。None、CommonJS、AMD、System、UMD、ES6(ES2015)、ES2020、ES2022、ESNext、Node16、NodeNext。moduleResolution:模块解析策略。打包器场景常用bundler;Node 原生 ESM、CJS 互操作常用node16、nodenext;旧文档中的node(即 node10)已逐渐不推荐。lib:指定编译时包含的类型库。比如:["ES2022", "DOM", "DOM.Iterable", "WebWorker"]。outFile:指定将输出文件合并为一个文件。outDir:指定输出文件夹。baseUrl:设置解析非相对模块名称的基本目录。paths:设置模块名到基于 baseUrl 的路径映射。如果使用paths,必须设置baseUrl。typeRoots:指定类型声明包的位置(默认包含node_modules/@types)。types:仅包含指定的@types包(设为[]可禁用自动引入)。
相关问题
什么是映射类型?
映射类型(Mapped Types)是基于一个旧类型,遍历它所有属性,按规则批量生成新类型。
type NewType<T> = {
[P in KeyUnion]: newType
}
type MyReadonly<T> = {
readonly [P in keyof T]: T[P]
};
什么是条件类型?
条件类型(Conditional Types)是 TypeScript 中一种基于类型关系进行条件判断的高级类型工具,类似于 JavaScript 中的三元运算符,但是作用于类型层面——根据类型之间的关系动态选择最终的类型。
T extends U ? X : Y
type ElementType<T> = T extends any[] ? T[number] : T;
type A = ElementType<string[]>; // string
type B = ElementType<number>; // number
interface 和 type 有什么区别?
interface:是声明,专注描述对象结构。可以被重复定义并自动合并,可以使用 extends 扩展,支持声明合并。
type:是别名,专注描述类型关系。可以给任意类型起名(原始、联合、元组、对象),使用联合类型、交叉类型实现扩展,不支持声明合并。
两者主要区别:
核心区别:interface 是声明,可以被重复定义并自动合并;type 是别名,一旦定义就不能再改变。
独有能力:interface 支持声明合并、扩展第三方库、与类实现更契合;type 支持联合类型、交叉类型、映射类型、条件类型、模板字面量类型。
- 扩展方式:interface 用
extends扩展;type 用&交叉类型组合。
选型建议:需要声明合并或给第三方库补类型时用 interface;需要联合、交叉、映射、条件类型等「类型运算」时用 type。普通对象结构两者都行,团队内保持一致即可。