Typescript基础及结合React实践(一) 从零搭出可跑的项目
接手一个要用 TypeScript 重写的中后台项目时,我心里想的是「不就是给变量加个冒号吗」。结果第一天就卡在 document.getElementById('btn').style.color = 'blue' 上,编译器红了一整行。真正难受的从来不是语法,是你得开始向编译器解释自己在干什么,而它比你严格得多。
这篇是我从安装 tsc 开始,把基本类型、函数、类、接口、泛型一路啃完,再手写 webpack 配置把 TypeScript 和 React 接起来的完整记录。全程不用脚手架,每一行配置都能看清楚它在解决什么问题。跟完之后你手上会有一个能跑的 TS + React + Redux + React Router 最小项目。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
tsc的安装、tsc --init生成的tsconfig.json里那几个关键选项在管什么- 布尔、数字、字符串、数组、元组、枚举、
any、null/undefined、void、never这一整套基本类型,以及any/void/never三者到底差在哪 - 函数的可选参数、默认参数、剩余参数和函数重载在 TypeScript 里的真实语义
- 类的定义、继承、
public/protected/private三个修饰符、静态成员和抽象类 - 接口怎么分别去规范对象、函数、数组和类,以及接口继承接口
- 泛型在函数和类上的用法,还有「有了接口为什么还要抽象类」
- 手写 webpack +
ts-loader搭出 TS + React 环境,包括类型定义文件@types/*是怎么回事 - 用 TypeScript 写 React 类组件,给 props 和 state 加接口约束
- Redux 的 action、reducer、store 全链路类型约束,以及
combineReducers合并和路由同步到仓库
先说清楚一件事,这篇写于 2018 年底,当时 TypeScript 才 3.2,React 是 16.6,脚手架生态跟现在差得远。原文的写法我一律保留,涉及到现在做法不一样的地方,我会另起一小段说明,具体行为以官方文档为准。这三篇是一个连载,第二篇 Typescript总结篇(二) 把语法细节铺得更全,第三篇 Typescript+React模板搭建(三) 则是带完整截图的工程模板实操。
# 一、装上 tsc,把编译链路先跑通
学 TypeScript 的第一步不是背类型,是先建立一个「改完 .ts 立刻能看到 .js 产物」的循环。看不到产物,你就永远不知道那些类型标注最后到底去哪了。
TypeScript 的编译器叫 tsc,全局装一个就行。
npm i typescript -g
全局安装完成后,我们新建一个
hello.ts的ts文件
// hello.ts内容
let a = "poet"
接下来我们在命令行输入
tsc hello.ts来编译这个ts文件,然后会在同级目录生成一个编译好了的hello.js文件
// hello.js内容
var a = "poet";
看到这个产物你就明白 TypeScript 干了什么了。let 被降级成了 var(因为默认 target 是 ES3/ES5),类型标注一个不剩,全部被擦掉。TypeScript 的类型只活在编译期,运行时是彻底不存在的,这一点后面讲 never、讲接口的时候会反复用到。
那么我们每次都要输
tsc hello.ts命令来编译,这样很麻烦,能否让它自动编译?答案是可以的,使用vscode来开发,需要配置一下vscode就可以。
首先我们在命令行执行
tsc --init来生成配置文件,然后我们在目录下看到生成了一个tsconfig.json文件
下面这张就是 tsc --init 之后目录里多出来的 tsconfig.json,它是整个项目类型检查行为的总开关。

这个
json文件里有很多选项
target是选择编译到什么语法module则是模块类型outDir则是输出目录,可以指定这个参数到指定目录
这三个是最先要动的。target 决定产物语法版本,写 es5 就是给老浏览器用,写 es2018 以上箭头函数、async 都会被原样保留,产物体积小很多。module 决定模块系统,Node 端配 commonjs,走打包器就配 esnext 交给 webpack 自己处理。outDir 单纯是产物落哪个目录。
新版本还多了一个当年没有的 moduleResolution。现在如果你的代码是交给 Vite、webpack、esbuild 这类打包器处理的,官方推荐值是 bundler,它比老的 node 解析策略更贴近打包器的真实行为,比如允许省略扩展名同时又支持 exports 字段。具体取值和兼容矩阵以官方文档为准。
更多细节 https://zhongsp.gitbooks.io/typescript-handbook/content/doc/handbook/tsconfig.json.html
接下来我们需要开启监控了,在
vscode任务栏中
有了配置文件还不够,得让它自动跑起来。VS Code 内置了 TypeScript 的构建任务,在任务栏里选 tsc: 监视 就能进入 watch 模式,改完保存自动编译。

做完这一步你应该看到终端持续输出编译日志,随便在 .ts 里写个类型错误,终端会立刻报出来。如果没有反应,大概率是 tsconfig.json 的 include 没把你的文件圈进去。
Typescript在线编辑器
建议使用在线编辑器练习 http://www.typescriptlang.org/play/index.html
这个 Playground 到现在还在维护,左边写 TS 右边实时出 JS,还能切 TypeScript 版本。学类型的时候比在本地建项目快得多,我到现在验证一个类型推断结果还是习惯先开它。
# 二、数据类型
js是弱类型语言,强弱类语言有什么区别呢?typescript最大的优点就是类型检查,可以帮你检查你定义的类型和赋值的类型。
弱类型的意思是变量的类型可以随时变,let a = 1 之后 a = '1' 完全合法,出错要等到运行时某个方法调不到才暴露。TypeScript 做的事就是在编译期先把这类问题拦下来。下面这一节把常用类型过一遍,每个类型我都补了一句「它到底在什么场景救你」。
# 2.1 布尔类型boolean
下面这段把同一个变量在 JS、TS、Java 三种语言里的写法摆在一起,看的是「类型信息写在哪」。JS 里根本没地方写,TS 是在变量名后面加冒号,Java 是写在变量名前面。
// 在js中,定义isFlag为true,为布尔类型boolean
let isFlag = true;
// 但是我们也可以重新给它赋值为字符串
isFlag = "hello swr";
// 在ts中,定义isFlag为true,为布尔类型boolean
// 在变量名后加冒号和类型,如 :boolean
let isFlag:boolean = true
// 重新赋值到字符串类型会报错
isFlag = "hello swr"
// 在java中,一般是这样定义,要写变量名也要写类型名
// int a = 10;
// string name = "poetries"
# 2.2 数字类型number
TypeScript 里没有 int / float / double 之分,所有数字都是 number,跟 JS 的运行时行为完全一致。NaN 和 Infinity 也算 number,这点偶尔会坑人。
let age:number = 28;
age = 29;
# 2.3 字符串类型string
let name:string = "poetries"
name = "iamswr"
这里有个坑要注意。如果你的 tsconfig.json 里 lib 带了 dom,全局作用域下已经存在一个 name 变量(Window.name),在顶层直接写 let name: string 有可能撞上「重复声明」的报错。放在模块里(文件中有任意一个 import 或 export)就没这个问题。我第一次遇到的时候排查了挺久,最后发现只要把文件变成模块就好了。
以上
boolean、number、string类型有个共性,就是可以通过typeof来获取到是什么类型,是基本数据类型
那么复杂的数据类型是怎么处理的呢?
# 2.4 数组 Array
数组有两种写法,T[] 和 Array<T>,语义完全一样,选哪个纯看团队约定。真正需要想清楚的是「里面允许放什么」,这决定了你后面取出来的元素能直接调什么方法。
// 数组
// 这是一个字符串数组,只能往里面放字符串,写别的类型会报错
let persion:string[] = ['poetries', 'jing']
// 另一个写法
let persions:Array<string> = ['poetries', 'jing']
// 如果数组里放对象呢
let persionObject:Array<object> = [{name:'poetries',age:22}]
let persionObjects:object[] = [{name:'poetries',age:22}]
// 在数组中放string、number、boolean、object
let arr:Array<number|object|string|boolean> = [22, 'test', true, {name:'poetries'}]
// 数组中放什么都可以
let arrAny:Array<any> = ['test',12,false]
联合类型的数组 Array<number|object|string> 用起来其实挺别扭,因为取出来的每一项都是联合类型,你想调 .toFixed() 得先做类型收窄。所以真实项目里更常见的是给数组元素定一个接口,而不是堆联合类型。至于 Array<any>,能不写就不写,理由下面 2.7 会讲。
# 2.5 元组类型tuple
元组解决的是这么一类场景:一个数组里每个位置的含义都不同,位置本身就是语义。最典型的就是 React Hooks 里 useState 的返回值,第 0 位是值第 1 位是 setter,这就是元组。
- 什么是元组类型?其实元组是数组的一种。
- 有点类似解构赋值,但是又不完全是解构赋值,比如元组类型必须一一对应上
- 元组类型是一个不可变的数组,长度、类型是不可变的
// 元组类型tuple
// 什么是元组类型?其实元组是数组的一种
let per :[string,number,object] = ['poetries',22,{love: 'coding'}]
原文说元组「不可变」,这个说法要打个补丁。TypeScript 会检查赋值和索引访问,但 per.push('x') 在早期版本里是不拦的,运行时长度照样能变。真想锁死,得用后来才有的 as const 或者 readonly [string, number]。这个我踩过,当时以为编译器保证了不变,结果线上数据多塞了一项都没人发现。
# 2.6 枚举类型enum
什么是枚举?枚举有点类似一一列举,一个一个数出来。一般用于值是某几个固定的值
// 枚举类型enum
enum sex {
BOY='男孩',
GIRL='女孩'
}
console.log(sex)