Typescript实践总结 从基础类型到工程化与项目实战
刚上手 TypeScript 那阵子,我最难受的不是语法,而是「编译器为什么要拦我」。明明 let a: void = 'hi' 看着挺正常,它就是报错;明明 arguments 用得好好的,标成 number[] 就红一片。后来我发现,绝大多数卡壳都不是记不住正确写法,而是不知道哪些写法是错的、错在哪一步。
这篇是我把 TypeScript 从基础类型一路啃到工程落地的完整笔记,按「正确的写法 / 错误的写法」成对整理,每个坑都标了 ❌。内容分三块,基础篇讲类型系统本身,工程篇讲声明文件和 tsconfig,实战篇讲 React 和 Vue 里真正会遇到的类型问题。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- TypeScript 的基本类型体系,以及它比 ES6 多出来的
void、any、never、元组、枚举 - 接口的四种属性形态(确定、可选、任意、只读)和它们互相之间的约束关系
- 函数的四种声明方式、重载,以及
interface和type到底该怎么选 - 泛型、泛型约束、索引类型、映射类型、条件类型这一整条「类型编程」链路
- 类型推断和四种类型保护(
typeof/instanceof/in/ 类型谓词)的适用场景 - 命名空间、三斜线指令、三类库(全局库 / 模块库 / UMD 库)声明文件怎么写
tsconfig.json每一个常用编译选项的含义,以及多项目工程引用怎么配- React 函数组件、类组件、高阶组件、Hooks、Redux 的类型写法,和 Vue 的
.vue声明文件
文章写于 2019 年,当时 TypeScript 还是 3.x,现在已经到 5.x 了。老写法我一律保留原样,需要更新认知的地方我会另起一小段说明现在一般怎么做,具体版本行为以官方文档为准。
# 一、基础篇,类型系统本身
先看这块。基础篇要解决的问题只有一个,把 JavaScript 里那些「运行时才炸」的错误挪到编译期。下面这几张图是我当时整理的思维导图,把基础篇涉及的类型、接口、泛型、类型检查机制串了一遍,可以先扫一眼有个整体印象,后面每一节都是对图里某个分支的展开。

# 1.1 基本类型
JavaScript 的类型分为两种,原始数据类型和对象类型。原始数据类型包括布尔值、数值、字符串、null、undefined 以及 ES6 中的新类型 Symbol。这一节主要看前五种原始数据类型在 TypeScript 里怎么标注,布尔值是最基础的一个,用 boolean 定义。
把两份类型清单摆在一起看,差异一眼就出来了。TypeScript 并没有替换掉 JS 的运行时类型,它是在 ES6 那套类型之上又叠了一层「只在编译期存在」的类型,void、any、never、元组、枚举、高级类型都属于这一层。编译完这些标注全部被擦掉,产物还是普通 JS。
ES6数据类型
BooleanNumberStringArrayFunctionObjectSymbolundefinednull
Typescript数据类型
BooleanNumberStringArrayFunctionObjectSymbolundefinednullvoidanynever- 元组
- 枚举
- 高级类型
下面这段把五种原始类型的标注方式一次性过完,重点看 Boolean 和 boolean 的区别,大写开头的是构造函数产出的包装对象,小写的才是原始值类型,这两个混用是新手最常踩的第一个坑。
正确的写法
➖➖➖➖➖➖➖➖➖布尔➖➖➖➖➖➖➖➖➖
// 布尔值
let isDone: boolean = false;
// 事实上 `new Boolean()` 返回的是一个 `Boolean` 对象
let createdByNewBoolean: Boolean = new Boolean(1);
//(直接调用 `Boolean` 也可以返回一个 `boolean` 类型)
let createdByBoolean: boolean = Boolean(1);
➖➖➖➖➖➖➖➖➖数值➖➖➖➖➖➖➖➖➖
// 数值
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
// ES6 中的二进制表示法
let binaryLiteral: number = 0b1010;
// ES6 中的八进制表示法
let octalLiteral: number = 0o744;
let notANumber: number = NaN;
let infinityNumber: number = Infinity;
➖➖➖➖➖➖➖➖➖字符串➖➖➖➖➖➖➖➖➖
let myName: string = 'Tom';
➖➖➖➖➖➖➖➖➖空值➖➖➖➖➖➖➖➖➖
// 没有返回值的函数为void
function alertName(): void {
alert('My name is Tom');
}
//声明一个 void 类型的只能将它赋值为 undefined 和 null
let unusable: void = undefined;
➖➖➖➖➖➖➖➖➖Null 和 Undefined➖➖➖➖➖➖➖➖➖
// undefined 类型的变量只能被赋值为 undefined,null 类型的变量只能被赋值为 null
let u: undefined = undefined;
let n: null = null;
正确写法记起来没难度,真正卡人的是下面这些。我整理笔记的时候特意把错误示例单独列一栏,因为编译器报错的时候你需要的是「哦,这个我见过」,而不是重新推一遍类型规则。
错误的写法
注意:正确的很好记,大多数人都会写正确的,关键是要记住这些错误的!!!
➖➖➖➖➖➖➖➖➖布尔➖➖➖➖➖➖➖➖➖
// 注意,使用构造函数 `Boolean` 创造的对象不是布尔值
let createdByNewBoolean: boolean = new Boolean(1);❌
➖➖➖➖➖➖➖➖➖数值➖➖➖➖➖➖➖➖➖
let decLiteral: number = "6";❌
➖➖➖➖➖➖➖➖➖字符串➖➖➖➖➖➖➖➖➖
let myName: string = 999;❌
➖➖➖➖➖➖➖➖➖空值➖➖➖➖➖➖➖➖➖
// 没有返回值的函数为void
function alertName(): void {❌
return 666;
}
//声明一个 void 类型的只能将它赋值为 undefined 和 null
let unusable: void = 'I love you';❌
➖➖➖➖➖➖➖➖➖Null 和 Undefined➖➖➖➖➖➖➖➖➖
// undefined 类型的变量只能被赋值为 undefined,null 类型的变量只能被赋值为 null
let u: undefined = 888;❌
let n: null = 999;❌
这里有个坑要注意,let unusable: void = null 在默认配置下是允许的,但只要你打开 strictNullChecks,null 就不再是所有类型的子类型,这行立刻变成错误。老项目升级的时候,这一个开关能给你炸出几百个报错,建议单独开一个分支慢慢改,别和业务需求混在一起。
# 1.2 任意值
any 是 TypeScript 给你留的逃生舱。它的语义是「这个位置我不检查了」,所以赋什么值都行,访问什么属性也都不报错。
正确的写法
// 顾名思义,可以被任何值赋值
let anyThing: any = 'hello';
let anyThing: any = 888;
let anyThing: any = true;
let anyThing: any = null;
let anyThing: any = undefined;
// 变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型:
let any;
any =true;
any 用多了 TypeScript 就退化成了 JavaScript,这是老生常谈。我自己的感受是,any 真正危险的地方不在于它自己不检查,而在于它会顺着调用链往外传染:一个返回 any 的函数,它的返回值再往下传三层,这三层全部失去保护,而你在编辑器里看不出任何异常。
顺着上面聊,TS 3.0 之后其实有一个更好的选择叫 unknown。它同样能接住任意值,但你在使用它之前必须先做类型收窄(用 typeof、类型断言或者类型谓词),编译器强制你把「我不知道这是什么」这件事显式处理掉。接口返回值、JSON.parse 的结果、第三方库的回调参数,这几个位置用 unknown 比 any 稳得多。
# 1.3 类型推论
不写类型标注不代表没有类型。TypeScript 会在变量初始化的那一刻,根据右边的值反推一个类型出来钉死,之后再赋别的类型就报错。
正确的写法
// 如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。
let myFavoriteNumber = 'seven';
//等价于
let myFavoriteNumber :string= 'seven';
错误的写法
// 第一句已经被推论为String类型了
let myFavoriteNumber = 'seven';
myFavoriteNumber = 7;❌
这里有个细节很多人没注意到:用 let 声明时推断出的是宽泛类型 string,用 const 声明时推断出的是字面量类型 'seven'。前者能再赋别的字符串,后者连别的字符串都不行。这个差异在写联合类型和配置对象的时候会反复咬人,比如 const config = { method: 'GET' } 里 method 推出来是 string 而不是 'GET',传给要求 'GET' | 'POST' 的函数就会报错。老办法是加 as const,TS 4.9 之后还多了 satisfies 这个更精细的写法。
# 1.4 联合类型
一个值可能是字符串也可能是数字,这种场景太常见了。联合类型用竖线连接,语义是「取值可以是这几种类型中的一种」。
正确的写法
// 联合类型(Union Types)表示取值可以为多种类型中的一种。
// 当你允许某个变量被赋值多种类型的时候,使用联合类型,管道符进行连接
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
// 也可用于方法的参数定义, 都有toString方法,访问 string 和 number 的共有属性是没问题的
function getString(something: string | number): string {
return something.toString();
}
错误的写法
// number类型没有length属性.所以编译错误,因为我们只能访问此联合类型的所有类型里共有的属性或方法:
function getLength(something: string | number): number {❌
return something.length;
}