0-引言
0.1JavaScript的隐式类型转换:
[] == '' //true
复制代码
隐式类型转换的规则是当 == 操作符两侧的值不满足恒等时(===),则先将空数组转换为字符串类型,然后再进行恒等比较。
相对于 JavaScript 应用而言,TypeScript 使得许多低级的 Bug 在开发阶段就能被检测出来并得到快速解决,显著提升了项目的整体质量和稳定性。
0.2Typescript的本质
- TypeScript 与 JavaScript 本质并无区别,你可以将 TypeScipt 理解为是一个添加了
类型注解
的 JavaScript。 - 此外,TypeScript 是一门
中间语言
,最终它还需要转译为纯 JavaScript,再交给各种终端解释、执行。
0.3Typescript更加可靠
当我们收到 Sentry(一款开源的前端错误监控系统)告警,关于“’undefined’ is not a function”“Cannot read property ‘xx’ of null|undefined” 之类的低级错误统计信息基本没有。而这正得益于 TypeScript 的静态类型检测,让至少 10% 的 JavaScript 错误(主要是一些低级错误)能在开发阶段就被发现并解决。
接手复杂的大型应用时,TypeScript 能让应用易于维护、迭代,且稳定可靠,也会让你更有安全感。
0.4面向接口编程
编写 TypeScript 类型注解,本质就是接口设计
。
interface IUserInfo {
/** 用户 id */
id: number;
/** 用户名 */
name: string;
/** 头像 */
avatar?: string;
}
function UserInfo(props: IUserInfo) {
...
}
复制代码
TypeScript 极大可能改变你的思维方式,从而逐渐养成一个好习惯。比如,编写具体的逻辑之前,我们需要设计好数据结构、编写类型注解,并按照这接口约定实现业务逻辑
。这显然可以减少不必要的代码重构,从而大大提升编码效率。
同时,你会更明白接口约定的重要性
,也会约束自己/他人设计接口、编写注解、遵守约定。
融会贯通,吃透原理,深入理解和吸收。
1-typescript基础入门
1.1搭建 TypeScript 学习开发环境
如果当前应用目录中安装了与内置服务不同版本的 TypeScript,我们就可以点击 VS Code 底部工具栏的版本号信息,从而实现 “use VS Code’s Version” 和 “use Workspace’s Version” 两者之间的随意切换。
我们也可以在当前应用目录下的 “.VS Code/settings.json” 内添加命令(如下所示)配置 VS Code 默认使用应用目录下安装的 TypeScript 版本,以便提供语法解析和类型检测服务。
{
"typescript.tsdk": "node_modules/typescript/lib"
}
复制代码
快捷键:通过 “Ctrl + ` ” 快捷键打开 VS Code 内置的命令行工具
1.1.1typescript的安装
全局安装
npm i -g typescript
复制代码
安装某个版本的
npm i -g typescript@版本号
复制代码
安装完成,通过以下命令查看版本:
tsc -v
复制代码
我们也可以通过安装在 Terminal 命令行中直接支持运行 TypeScript 代码(Node.js 侧代码)的 ts-node
来获得较好的开发体验。
全局安装ts-node
npm i -g ts-node
复制代码
1.1.2编写hello typescript
我们可以在练习目录下输入“tsc –init”命令快速创建一个 tsconfig.json 文件,或者在 VS Code 应用窗口新建一个空的 tsconfg.json配置 TypeScript 的行为。
tsc --init
复制代码
为了让 TypeScript 的行为更加严格、简单易懂,降低学习的心理负担,这就要求我们在 tsconfig.json
中开启如下所示设置,该设置将决定了 VS Code 语言服务如何对当前应用下的 TypeScript 代码进行类型检测。
{
"compilerOptions": {
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"strictNullChecks": true, /* Enable strict null checks. */
"strictFunctionTypes": true, /* Enable strict checking of function types. */
"strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
"strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
"alwaysStrict": false, /* Parse in strict mode and emit "use strict" for each source file. */
}
}
复制代码
新建一个 HelloTypescript.ts 文件:
function say(word: string) {
console.log(word);
}
say('Hello, typescript');
复制代码
.ts 文件创建完成后,我们就可以使用 tsc(TypeScript Compiler) 命令将 .ts 文件转译为 .js 文件。
注意: 指定转译的目标文件后,tsc 将忽略当前应用路径下的 tsconfig.json 配置,因此我们需要通过显式设定如下所示的参数,让 tsc 以严格模式检测并转译 TypeScript 代码。
tsc HelloWorld.ts --strict --alwaysStrict false
复制代码
同时,我们可以给 tsc 设定一个 watch 参数监听文件内容变更,实时进行类型检测和代码转译,如下代码所示:
tsc HelloWorld.ts --strict --alwaysStrict false --watch
复制代码
我们也可以直接使用 ts-node 运行 HelloTypescript.ts
,如下代码所示:
ts-node HelloTypescript.ts
复制代码
当然,我们也可以唤起“直接运行”(本质上是先自动进行转译,再运行)TypeScript 的 ts-node 命令行来编写代码,这就跟我们在 Node.js 命令行或者浏览器中调试工具一样。
> ts-node
> function say(word: string) {
> console.log(word);
> }
> say('Hello, World');
Hello, World
undefined
复制代码
这里请注意: TypeScript 的类型注解旨在约束函数或者变量,在上面的例子中,我们就是通过约束一个示例函数来接收一个字符串类型(string)的参数。
1.1.3小结
VS Code 让我们获得一种较为理想的开发体验,不必等到转译阶段,在编码时就能快速检测、抛出类型错误,极大地提升了 TypeScript 开发体验和效率。
特别需要注意的是,VS Code 默认使用自身内置的 TypeScript 语言服务版本,而在应用构建过程中,构建工具使用的却是应用路径下 node_modules/typescript 里的 TypeScript 版本。如果两个版本之间存在不兼容的特性,就会造成开发阶段和构建阶段静态类型检测结论不一致的情况,因此,我们务必将 VS Code 语言服务配置成使用当前工作区的 TypeScript 版本。
1.2简单基础类型
从某种意义上来说,TypeScript 其实就是 JavaScript 的超集。
除了常量枚举(const enums),TypeScript 就是在 JavaScript 语法基础上添加了类型注解。
1.2.1基本语法
我们可以把 TypeScript 代码的编写看作是为 JavaScript 代码添加类型注解。
在 TypeScript 语法中,类型的标注主要通过类型后置语法
来实现。
let num: number = 1;
复制代码
特殊说明:number表示数字类型,:用来分割变量和类型的分隔符
。
关于 JavaScript 原始数据类型到 TypeScript 类型的映射关系如下表所示:
在 JavaScript 中,原始类型指的是非对象且没有方法的数据类型,它包括 string、number、bigint、boolean、undefined 和 symbol 这六种 (null 是一个伪原始类型,它在 JavaScript 中实际上是一个对象,且所有的结构化类型都是通过 null 原型链派生而来)。
1.2.2字符串
在 JavaScript 中,我们可以使用string表示 JavaScript 中任意的字符串(包括模板字符串),具体示例如下所示:
let firstname: string = 'Captain'; // 字符串字面量
let familyname: string = String('S'); // 显式类型转换
let fullname: string = `my name is ${firstname}.${familyname}`; // 模板字符串
复制代码
1.2.3数字
/** 十进制整数 */
let integer: number = 6;
/** 十进制整数 */
let integer2: number = Number(42);
/** 十进制浮点数 */
let decimal: number = 3.14;
/** 二进制整数 */
let binary: number = 0b1010;
/** 八进制整数 */
let octal: number = 0o744;
/** 十六进制整数 */
let hex: number = 0xf00d;
复制代码
如果使用较少的大整数,那么我们可以使用bigint类型
来表示,如下代码所示。
let big: bigint = 100n;
复制代码
请注意: 虽然number和bigint都表示数字,但是这两个类型不兼容。
1.2.4布尔值
/** TypeScript 真香 为 真 */
let TypeScriptIsGreat: boolean = true;
/** TypeScript 太糟糕了 为 否 */
let TypeScriptIsBad: boolean = false
复制代码
1.2.5Symbol
let sym1: symbol = Symbol();
let sym2: symbol = Symbol('42');
复制代码
当然,TypeScript 还包含 Number、String、Boolean、Symbol 等类型(注意区分大小写)。
特殊说明: 请你千万别将它们和小写格式对应的 number、string、boolean、symbol 进行等价。
let sym: symbol = Symbol('a');
let sym2: Symbol = Symbol('b');
sym = sym2 //fail “symbol”是基元,但“Symbol”是包装器对象。如可能首选使用“symbol”。
//Type 'Symbol' is not assignable to type 'symbol'.
// 'symbol' is a primitive, but 'Symbol' is a wrapper object. Prefer using 'symbol' when possible.
sym2 = sym // ok
let str: String = new String('a');
let str2: string = 'a';
str = str2; // ok
str2 = str; // fail
复制代码
1.2.6静态类型检测
在编译时期,静态类型的编程语言即可准确地发现类型错误,这就是静态类型检测的优势。
在编译(转译)时期,TypeScript 编译器将通过对比检测变量接收值的类型与我们显示注解的类型,从而检测类型是否存在错误。如果两个类型完全一致,显示检测通过;如果两个类型不一致,它就会抛出一个编译期错误,告知我们编码错误,具体示例如下代码所示:
const trueNum: number = 42;
const fakeNum: number = "42"; // ts(2322) Type 'string' is not assignable to type 'number'.
复制代码
在编写代码的同时,我们可以同步进行静态类型检测(无须等到编译后再做检测),极大地提升了开发体验和效率。
1.3复杂基础类型
1.3.1数组类型(Array)
首先,我们可以直接使用 []
的形式定义数组类型,如下代码所示:
/** 子元素是数字类型的数组 */
let arrayOfNumber: number[] = [1, 2, 3];
/** 子元素是字符串类型的数组 */
let arrayOfString: string[] = ['x', 'y', 'z'];
复制代码
同样,我们也可以使用 Array 泛型
定义数组类型,如下代码所示:
/** 子元素是数字类型的数组 */
let arrayOfNumber: Array<number> = [1, 2, 3];
/** 子元素是字符串类型的数组 */
let arrayOfString: Array<string> = ['x', 'y', 'z'];
复制代码
更推荐使用 [] 这种形式来定义。一方面可以避免与 JSX 的语法冲突,另一方面可以减少不少代码量。
1.3.2元组类型(Tuple)
元组最重要的特性是可以限制数组元素的个数和类型,它特别适合用来实现多值返回。
我们熟知的一个使用元组的场景是 React Hooks.
import { useState } from 'react';
function useCount() {
const [count, setCount] = useState(0);
return ....;
}
复制代码
出于较好的扩展性、可读性和稳定性考虑,我们往往会更偏向于把不同类型的值通过键值对的形式塞到一个对象中,再返回这个对象(尽管这样会增加代码量),而不是使用没有任何限制的数组。比如我们可能会使用如下的对象结构来替换数组:
{
state,
setState
}
复制代码
而 TypeScript 的元组类型正好弥补了这个不足,使得定义包含固定个数元素、每个元素类型未必相同的数组成为可能。
(需要注意的是,毕竟 TypeScript 会转译成 JavaScript,所以 TypeScript 的元组无法在运行时约束所谓的“元组”像真正的元组一样,保证元素类型、长度不可变更)。
对于 TypeScript 而言,如下所示的两个元组类型其实并不相同:
[State, SetState]
[SetState, State]
复制代码
所以添加了不同元组类型注解的数组后,在 TypeScript 静态类型检测层面就变成了两个不相同的元组,如下代码所示:
const x: [State, SetState] = [state, setState];
const y: [SetState, State] = [setState, state];
复制代码
比如 useState 的返回值类型是一个元组类型,如下代码所示(以下仅是简单的例子,事实上 useState 的类型定义更为复杂):
(state: State) => [State, SetState]
复制代码
元组相较对象而言,不仅为我们实现解构赋值提供了极大便利,还减少了不少代码量,这可能也是 React 官方如此设计核心 Hooks 的重要原因之一。
这里需要注意:数组类型的值只有显示添加了元组类型注解后(或者使用 as const,声明为只读元组),TypeScript 才会把它当作元组,否则推荐出来的类型就是普通的数组类型。
1.3.3几种特殊的类型
- any
any 指的是一个任意类型
,它是官方提供的一个选择性绕过静态类型检测的作弊方式。
我们可以对被注解为 any 类型的变量进行任何操作
,包括获取事实上并不存在的属性、方法,并且 TypeScript 还无法检测其属性是否存在、类型是否正确。
比如我们可以把任何类型的值赋值给 any 类型的变量,也可以把 any 类型的值赋值给任意类型(除 never 以外
)的变量,如下代码所示:
let anything: any = {};
anything.doAnything(); // 不会提示错误
anything = 1; // 不会提示错误
anything = 'x'; // 不会提示错误
let num: number = anything; // 不会提示错误
let str: string = anything; // 不会提示错误
复制代码
any 类型会在对象的调用链中进行传导
,即所有 any 类型的任意属性的类型都是 any,如下代码所示:
let anything: any = {};
let z = anything.x.y.z; // z 类型是 any,不会提示错误
z(); // 不会提示错误
复制代码
除非有充足的理由,否则我们应该尽量避免使用 any ,并且开启禁用隐式 any 的设置。
- unknown
主要用来描述类型并不确定
的变量。
比如在多个 if else 条件分支场景下,它可以用来接收不同条件下类型各异的返回值的临时变量
,如下代码所示:
let result: unknown;
if (x) {
result = x();
} else if (y) {
result = y();
} ...
复制代码
与 any 不同的是,unknown 在类型上更安全
。比如我们可以将任意类型的值赋值给 unknown,但 unknown 类型的值只能赋值给 unknown 或 any
,如下代码所示:
let result: unknown;
let num: number = result; // 提示 ts(2322)
let anything: any = result; // 不会提示错误
复制代码
使用 unknown 后,TypeScript 会对它做类型检测。但是,如果不缩小类型(Type Narrowing),我们对 unknown 执行的任何操作都会出现如下所示错误:
let result: unknown;
result.toFixed(); // 提示 ts(2571)
复制代码
而所有的类型缩小手段
对 unknown 都有效,如下代码所示:
let result: unknown;
if (typeof result === 'number') {
result.toFixed(); // 此处 hover result 提示类型是 number,不会提示错误
}
复制代码
- void、undefined、null
依照官方的说法,它们实际上并没有太大的用处.
首先我们来说一下 void 类型,它仅适用于表示没有返回值的函数
。即如果该函数没有返回值,那它的类型就是 void。
在 strict 模式下,声明一个 void 类型的变量几乎没有任何实际用处,因为我们不能把 void 类型的变量值再赋值给除了 any 和 unkown 之外的任何类型变量。
然后我们说说 undefined 类型 和 null 类型,它们是 TypeScript 值与类型关键字同名的唯二例外。但这并不影响它们被称为“废柴”,因为单纯声明 undefined 或者 null 类型的变量也是无比鸡肋,示例如下所示:
let undeclared: undefined = undefined; // 鸡肋
let nullable: null = null; // 鸡肋
复制代码
undefined 的最大价值主要体现在接口类型
上,它表示一个可缺省、未定义的属性。
这里分享一个稍微有点费解的设计:我们可以把 undefined 值或类型是 undefined 的变量赋值给 void 类型变量,反过来,类型是 void 但值是 undefined 的变量不能赋值给 undefined 类型。
const userInfo: {
id?: number;
} = {};
let undeclared: undefined = undefined;
let unusable: void = undefined;
unusable = undeclared; // ok
undeclared = unusable; // ts(2322)
复制代码
而 null 的价值
我认为主要体现在接口制定上
,它表明对象或属性可能是空值
。尤其是在前后端交互的接口,比如 Java Restful、Graphql,任何涉及查询的属性、对象都可能是 null 空对象,如下代码所示:
const userInfo: {
name: null | string
} = { name: null };
复制代码
除此之外,undefined 和 null 类型还具备警示意义
,它们可以提醒我们针对可能操作这两种(类型)值的情况做容错处理。
我们需要类型守卫(Type Guard)
在操作之前判断值的类型是否支持当前的操作。类型守卫既能通过类型缩小
影响 TypeScript 的类型检测,也能保障 JavaScript 运行时的安全性,如下代码所示:
const userInfo: {
id?: number;
name?: null | string
} = { id: 1, name: 'Captain' };
if (userInfo.id !== undefined) { // Type Guard
userInfo.id.toFixed(); // id 的类型缩小成 number
}
复制代码
我们不建议随意使用非空断言
来排除值可能为 null 或 undefined 的情况,因为这样很不安全。
userInfo.id!.toFixed(); // ok,但不建议
userInfo.name!.toLowerCase() // ok,但不建议
复制代码
而比非空断言更安全、类型守卫更方便的做法是使用单问号(Optional Chain)
、双问号(空值合并)
,我们可以使用它们来保障代码的安全性,如下代码所示:
userInfo.id?.toFixed(); // Optional Chain
const myName = userInfo.name?? `my name is ${info.name}`; // 空值合并
复制代码
- never’
never 表示永远不会发生值
的类。
首先,我们定义一个统一抛出错误的函数,代码示例如下(圆括号后 : + 类型注解 表示函数返回值的类型
)
function ThrowError(msg: string): never {
throw Error(msg);
}
复制代码
以上函数因为永远不会有返回值
,所以它的返回值类型就是 never。
never 是所有类型的子类型,它可以给所有类型赋值.除了 never 自身以外,其他类型(包括 any 在内的类型)都不能为 never 类型赋值。
let Unreachable: never = 1; // ts(2322)
Unreachable = 'string'; // ts(2322)
Unreachable = true; // ts(2322)
let num: number = Unreachable; // ok
let str: string = Unreachable; // ok
let bool: boolean = Unreachable; // ok
复制代码
在恒为 false 的类型守卫条件判断下,变量的类型将缩小为 never
(never 是所有其他类型的子类型,所以是类型缩小为 never,而不是变成 never
)。
const str: string = 'string';
if (typeof str === 'number') {
str.toLowerCase(); // Property 'toLowerCase' does not exist on type 'never'.ts(2339)
}
复制代码
我们可以把 never 作为接口类型下的属性类型
,用来禁止写接口下特定的属性
.
const props: {
id: number,
name?: never
} = {
id: 1
}
props.name = null; // ts(2322))
props.name = 'str'; // ts(2322)
props.name = 1; // ts(2322)
复制代码
- object
object 类型表示非原始类型的类型,即非 number、string、boolean、bigint、symbol、null、undefined 的类型。
declare function create(o: object | null): any;
create({}); // ok
create(() => null); // ok
create(2); // ts(2345)
create('string'); // ts(2345)
复制代码
1.3.4类型断言
TypeScript 类型检测无法做到绝对智能,毕竟程序不能像人一样思考。有时会碰到我们比 TypeScript 更清楚实际类型的情况,比如下面的例子
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2); // 提示 ts(2322)
复制代码
在 TypeScript 看来,greaterThan2 的类型既可能是数字,也可能是 undefined
,所以上面的示例中提示了一个 ts(2322) 错误,此时我们不能把类型 undefined 分配给类型 number。
不过,我们可以使用一种笃定的方式——类型断言
(类似仅作用在类型层面的强制类型转换)告诉 TypeScript 按照我们的方式做类型检查。
比如,我们可以使用 as 语法
做类型断言,如下代码所示:
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2) as number;
复制代码
又或者是使用尖括号 + 类型的格式
做类型断言,如下代码所示:
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = <number>arrayNumber.find(num => num > 2);
复制代码
尖括号格式会与 JSX 产生语法冲突
,因此我们更推荐使用 as 语法
。
注意: 类型断言的操作对象必须满足某些约束关系
,否则我们将得到一个 ts(2352) 错误,即从类型“源类型”到类型“目标类型”的转换是错误的,因为这两种类型不能充分重叠。
如果我们把它称为“指白马为马”“指马为白马”,就可以很贴切地体现类型断言的约束条件:父子、子父类型之间可以使用类型断言进行转换
。
注意:这个结论完全适用于复杂类型,但是对于 number、string、boolean 原始类型来说,不仅父子类型可以相互断言,父类型相同的类型也可以相互断言
,比如 1 as 2、’a’ as ‘b’、true as false(这里的 2、’b’、false 被称之为字面量类型
),反过来 2 as 1、’b’ as ‘a’、false as true 也是被允许的(这里的 1、’a’、true 是字面量类型),尽管这样的断言没有任何意义。
另外,any 和 unknown 这两个特殊类型属于万金油
,因为它们既可以被断言成任何类型,反过来任何类型也都可以被断言成 any 或 unknown。因此,如果我们想强行“指鹿为马”,就可以先把“鹿”断言为 any 或 unknown,然后再把 any 和 unknown 断言为“马”,比如鹿 as any as 马。
我们除了可以把特定类型断言成符合约束添加的其他类型之外,还可以使用“字面量值 + as const”语法结构
进行常量断言
.
/** str 类型是 '"str"' */
let str = 'str' as const;
/** readOnlyArr 类型是 'readonly [0, 1]' */
const readOnlyArr = [0, 1] as const;
复制代码
此外还有一种特殊非空断言,即在值(变量、属性)的后边添加 '!' 断言操作符
,它可以用来排除值为 null、undefined 的情况.
let mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // ts(2531)
复制代码
对于非空断言来说,我们同样应该把它视作和 any 一样危险的选择。
在复杂应用场景中,如果我们使用非空断言,就无法保证之前一定非空的值,比如页面中一定存在 id 为 feedback 的元素,数组中一定有满足 > 2 条件的数字,这些都不会被其他人改变。而一旦保证被改变,错误只会在运行环境中抛出,而静态类型检测是发现不了这些错误的。
所以,我们建议使用类型守卫来代替非空断言
.
let mayNullOrUndefinedOrString: null | undefined | string;
if (typeof mayNullOrUndefinedOrString === 'string') {
mayNullOrUndefinedOrString.toString(); // ok
}
复制代码
到这里,TypeScript 所有的基础类型
就交代完了,反复消化,夯实基础…(未完待续)