- TypeScript学习第一章:TypeScript初识
- TypeScript学习第二章:为什么使用TypeScript?
- TypeScript学习第三章: 常用类型
- TypeScript学习第四章: 类型缩小
- TypeScript学习第五章: 函数
- TypeScript学习第六章: 对象类型
- TypeScript学习第七章: 类型操纵
- TypeScript学习第八章: 类
- TypeScript学习第九章:模块
TypeScript学习第一章:TypeScript初识
1.1 TypeScript学习初见
TypeScript(TS)是由微软Microsoft由2012年推出的自由和开源的编程语言, 目前主流的三大框架React 、Vue 和 Angular这三大主流框架再加上最新的鸿蒙3.0都可以用TS进行开发.
可以说 TS 是 JS 的超集, 是建立在JavaScript上的语言. TypeScript把其他语言的一些精妙的语法, 带入到JavaScript中, 让JS达到了一个新的高度。
可以在TS中使用JS以外的扩展语法, 同时可以结局TS对面向对象和静态类型的良好支持, 可以让我们编写更健壮、更可维护的大型项目。
1.2 TypeScript介绍
因为TypeScript是JavaScript的超集, 所以要介绍TS, 不得不提一下JS, JS从在引入编程社区20多年以来, 已经成了有史以来应用最广泛的跨平台语言之一了, 从一开始为网页中添加一些微不足道的、交互性的小型的脚本语言发展到现在各种规模的前端和后端应用程序的首选语言了.
虽然我们用JS语言编写程序的大小、范围和复杂性呈指数级的增长, 但是JS语言表达不同代码单元之间的关系和能力却很弱, 使得JS成了一项难以大规模管理的任务, 而且也很难解决程序员经常出现的错误: 类型错误.
而TS语言可以很好的解决这个错误, 他的目标是成为JS程序的静态类型检查器, 可以在代码运行之前进行检查, 也就是静态编译, 并且呢, 可以确保我们程序的类型正确(即进行类型检查).
TS添加了可选的静态类型和基于类的面向对象编程等等, 是JS的语言扩展, 不是JS的替代品, 会让JS前进的步伐更坚实、更遥远.
1.3 JS 、TS 和 ES之间的关系
ES6又称为ECMAScript 2015, TypeScript 是 JS 的超集, 他包含Javascript的所有元素, 能运行Javascript代码, 并扩展了JS语法, 并添加了静态类型 类 模块 接口 类型注解等等方面的功能, 更加易于大项目的开发.
这张图表示TS不仅包含了JS和ES的最新内容, 还扩展了新的功能.
总的来说, ECMAScript是JS的标准, TS是JS的超集.
1.4 TS的竞争者有哪些?
1. ESLint
2. TSlint
1 和 2 都是和TypeScript一样来突出代码中可能出现的错误, 至少i没有为检查过程添加新的语法, 但是这两者都不打算最为IDE集成的工具来运行, 这两个的存在可以是TS做更少的检查, 但是这些检查并不适合于所有的代码库。
3. CoffeeScript
CoffeeScript是想改进JS语言, 但是现在用的人少了, 因为他又成为了JS的标准, 属于是打不过JS了。
4.Flow
Vue2的源码的类型检查工具就是flow, 不过Vue3已经开始使用TS做类型检查了.
Flow更悲观的判断类型, 而TS更加乐观.
Flow是为了维护Facebook的代码库而建立的, 而TS是作为一种独立的语言而建立的, 其内部有独立的环境, 可以自由专注于工具的开发和整个生态系统的维护
TypeScript学习第二章:为什么使用TypeScript?
2.1 发现问题
JS中每个值都有一组行为, 我们可以通过运行不同的操作来观察:
// 在 'message' 上访问属性方法 'toLowerCase', 并调用它
message.toLowerCase();
// 调用 'message'
message();
我们尝试直接调用message, 但是假设我们不知道message, 我们就无法可靠的说出尝试运行任何的这些代码会得到什么结果, 每个操作的结果完全取决于我们最初给message的赋值. 我们编译代码的时候真的可以调用message()么, 也不一定有toLowerCase()这个方法, 而且也不知道他们的返回值是什么.
通常我们在编写js的时候需要对上面所述的细节牢记在心, 才能编写正确的代码。
假设我们知道了message 是什么,如下所示,但是第三行就会报错。
const message = 'Hello World'
message.toLowerCase(); // 输出hello world
message(); // TypeError: message is not a function
如果我们能避免这样的错误, 就完美了, 当我们运行我们的代码的时候, 选择做什么的方式, 是通过确定值的类型, 来确定他具有什么样的行为和功能的, TypeError
就暗指字符串是不能作为函数来调用的. 对于某些值, 比如string
和number
, 我们可以使用typeof来识别他们的类型.
但是对于像函数之类的其他的东西, 没有相应的运行时机制, 比如下面的代码, 运行是有条件的, 也就是说这个x是必须具有flip这个方法的, js只能在运行一下代码时才能知道这个x是提供了什么的, 我们如果能够使用静态类型系统, 在运行代码之前预测预期的代码,问题就解决了.
function fn(x) {
return x.flip()
}
2.2 静态类型检查
const message = 'hello'
message() // TypeError
上述这段代码会引起TypeError, 理想的情况下, 我们希望有一个工具可以在我们代码运行之前发现这些错误, TS就可以实现这些功能. 静态类型系统就描述了当前我们运行程序的时候, 值得形状和行为, 像TS这样的类型检查器, 会告诉我们什么时候代码会出现问题.
2.3 非异常故障
JS 在运行的时候会告诉我们他认为某些东西是没有意义的情况, 因为ECMA规范明确说明了JS在遇到某些意外情况下应该是如何表现得, 比如如下代码:
const user = {
name: "小千",
age:26,
};
user.location; // 返回undefined, 理应报错, 因为根本没有location这个属性
但是静态类型系统要求必须对调用哪些代码做系统的标记, 如果是在TS运行这段代码, 就会出现location未定义的错误, 如下图所示:
TS可以在开发过程中捕获很多类似于合法的错误, 比如说错别字, 未调用函数, 基本的逻辑错误等等:
拼写错误: 属性toLocaeleLowerCase在String类型中不存在, 你找的是否是toLocaleLowerCase属性?
未调用的函数检查: 运算符号 < 不能用在一个 '() => number' 和 number数字之间.
逻辑问题: value !== 'a' 和 value === 'b'逻辑重叠.
2.4 使用工具
- 安装VSCode
- 安装Node.js:使用命令
node -v
来检查nodejs版本 - 安装TypeScript编译器:
npm i typescript -g
然后我们要编译我们的TS, 因为TS是不能直接运行的, 我们必须把他编译成JS.
在终端中使用cls 或者 clear命令可以清屏
可以使用tsc命令来转换TS 成 JS: 例如 tsc hello.ts
, 就会生成对应的JS文件.
hello.ts:
// 你好, 世界
// console.log('Hello World')
// 会出现函数实现重复的错误
function greet(person, date) {
console.log(`Helo ${person}, today is ${date}`)
}
greet('xiaoqian','2021/12/04')
会出现函数实现重复的错误是因为hello.js也有这个greet的函数, 这是跟我们编译环境是矛盾的, 而且还需要我们重新编译ts, 所以我们需要进行优化编译过程.
2.5 优化编译
- 解决TS和JS冲突问题
tsc --init # 生成配置文件
- 自动编译
tsc --watch
- 发出错误
tsc --noEmitOnError hello.ts
TS文件编译成JS文件以后, 当出现函数名或者是变量名相同的时候, 会给我们提示重复定义的问题,可以通过 tsc --init
来生成一个配置文件来解决冲突问题. 先把严格模式strict关闭, 可解决未指定变量类型的问题.
当我们修改TS文件的时候, 我们需要重新的执行编译, 才能拿到最新的结果我们需要自动编译, 可以通过tsc --watch
来解决自动编译的问题.
当我们编译完之后, JS还是能正常运行的, 我们可以加一个noEmitOnError的参数来解决, 这样的话如果我们在TS中出现错误就可以让TS不编译成JS文件了.
最终的命令行指令是这样的:
tsc --watch --noEmitOnError
2.6 显式类型
刚才我们在tsconfig.json里把strict模式关闭了, 如果我们打开, 就会出现未指定变量类型的错误, 如果要解决这个问题, 我们就需要指定显式类型:
什么叫显式类型呢, 就是手工的给变量定义类型, 语法如下:
function greet(person: string, date: Date) {
console.log(`Helo ${person}, today is ${date.toDateString()}.`)
}
在TS中, 也不是必须指定变量的数据类型, TS会根据你的变量自动推断数据类型, 如果推断不出来就会报错.
2.7 降级编译
我们可以在tsconfig.json 就修改target来更改TS编译目标的代码版本.
{
"compilerOptions": {
......
"target": 'es5',
......
}
}
默认为es2016, 即es7, 建议以默认值就可以, 目前的浏览器都能兼容
2.8 严格模式
不同的用户使用TS在类型检查中希望检查的严格程度是不同的, 有的人喜欢更宽松的验证体验, 从而仅仅验证程序的某些部分, 并且仍然拥有不错的工具.
默认情况下:
{
"compilerOptions": {
......,
"strict": true, /* 严格模式: 启用所有严格的类型检查选项。*/
"noImplicitAny": true, /* 为隐含的'any'类型的表达式和声明启用错误报告。*/
"strictNullChecks": true, /* 当类型检查时,要考虑'null'和'undefined' */
......
}
}
一般来说使用TS就是追求的强立即验证, 这些静态检查设置的越严格, 越可能需要更多额外的编程工作, 但是从长远来说是值得的, 它会使代码更加容易维护. 如果可以我们应该始终打开这些类型检查.
启用strictNullChecks可以拦截null 和undefined 的错误, 启用noImplicitAny可以拦截any的错误, 启用strict可以拦截所有的严格类型检查选项, 包括前面两个的.
所以结论就是只需要开启"strict"为true即可, 当我们遇到
TypeScript学习第三章: 常用类型
3.1 基元类型string number 和 boolean
- string: 字符串, 例子: 'Hello', 'World'.
- number: 数字, 例子: 42, -100.
- boolean: 布尔, 例子: true, false.
String Number Boolean 也是合法的, 在TS里专门指一些很少的, 出现在代码里的一些特殊的内置类型, 对于类型我们始终使用小写的string, number 和 boolean.
为了输出方便我们可以在tsconfig.json的rootDir里设置一个目录"./src"
, 设置outDir为"./dist"
.
let str: string = 'hello typescript'
let num: number = 100
let bool: boolean = true
3.2 数组
数组的定义方法有两种:
- type[]
- Array
Array
let arr: number[] = [1, 4, 6 ,8]
// arr = ['a']
let arr2: Array<number> = [1, 2, 3]
arr2 = []
值得注意的是, 数组可以被赋值为空数组[], 但是不能被赋值为规定类型以外的数组值.
3.3 any
如果不希望某个特定值导致类型检查错误, 就可以使用any.
当一个值是any的时候, 可以访问它的任何属性, 将它分配给任何类型的值, 或者几乎任何其它语法上的东西都是合法的. 但是运行的时候该报错还是报错, 所以我们不应该经常使用他.
let obj: any = {
x: 0
}
obj.foo() // js调用时就会报错
obj()
obj.bar = 100
obj = 'hello'
const n: number = obj
3.4 变量上的类型解释
let myName: string = "Felixlu"
采用(冒号:) + (类型string)的方式.
let my: string = "Hello World"
// 如果不声明, 会自动推断
let myName = "Bleak" // 将myName推断成string
myName = 100 // 报错, 不能将number分配给string.
3.5 函数
function greet (name: string): void {
console.log("Hello," + name.toUpperCase() + "!!!")
}
const greet2 = (name: string): string =>{
return "你好," + name
}
greet("Bleak")
console.log(greet2("黯淡"))
第一个name: string是参数类型注释, 第二个: void是返回值类型注释.
一般来说不用定义返回值类型, 因为会自动推断.
const names = ["xiaoqian", 'xiaoha', 'xiaoxi']
names.forEach(function(s) {
console.log(s.toUpperCase());
})
names.forEach(s => {
console.log(s.toLowerCase());
})
匿名函数与函数声明有点不同, 当一个函数出现在出现在TS可以确定它如何被调用的地方的时候, 这个函数的参数会自动的指定类型.
3.6 对象类型
function printCoord(pt: {x: number; y: number}) {
console.log("坐标的x值是: " + pt.x)
console.log("坐标的y值是: " + pt.y)
}
printCoord({x: 3, y: 7})
对于参数类型注释是对象类型的, 对象中属性的分割可以用 分号; 或者 逗号,
function printName(obj: {first: string, last?: string}) {
if(obj.last === undefined) {
console.log("名字是:" + obj.first)
} else {
console.log("名字是:" + obj.first + obj.last)
}
}
printName({
first: "Mr.",
last: "Bleak"
})
使用?可以指定对象中某个参数可以选择传入或者不传入, 不传入其值就是undefined.
如何在函数体内确定某个带?的参数是否传参了呢?可以使用两种方法
-
if(obj.last === undefined) {// 未传入时的方法体 } else {// 传入时的方法体 }
-
console.log(obj.last?.toUpperCase())
第二种方式更加优雅, 更推荐使用
3.7 联合类型
let id: number | string
TS的类型系统允许我们使用多种运算符, 从现有类型中构建新类型union.
联合类型是由两个或多个其他类型组成的类型. 表示可能是这些类型中的任何一种的值, 这些类型中的每一种被称为联合类型的成员.
function printId(id: number | string) {
console.log("当前Id为:" + id)
// console.log(id.toUpperCase())
if (typeof id === 'string') {
console.log(id.toUpperCase())
} else {
console.log(id)
}
}
printId(101)
printId('202')
如果需要调用一些参数的属性或者方法, 可以使用JS携带的typeof函数来进行判断并分情况执行代码.
function welcomePeople(x: string[] | string) {
if(Array.isArray(x)) { // Array.isArray(x)可以测试x是否是一个数组
console.log("Hello, " + x.join(' and '))
} else {
console.log("Welcome lone traveler " + x)
}
}
welcomePeople(["A", "B"])
welcomePeople('A')
根据分支来进行操作的函数.
// 共享的方法
function getFirstThree(x: number[] | string) {
return x.slice(0, 3)
}
都有的属性和方法, 可以直接使用.
3.8 类型别名
type Point = {
x: number
y: number
} // 对象类型
function printCoord(pt: Point) {
}
printCoord({x: 100, y: 200})
type ID = number | string // 联合类型
function printId(id: ID) {
}
printId(100)
printId('2333')
type UserInputSanitizedString = string // 基元类型
function sanitizedString(str: string): UserInputSanitizedString {
return str.slice(0, 2)
}
let userInput = sanitizedString('hello')
console.log(userInput)
type可以用来定义变量的类型, 如果是对象, 里面的属性和方法可以用逗号, 分号; 或直接不写来做间隔, 可以用来做一些平时经常会用到的类型来做复用, 其可以用于变量的类型指定上.
3.9 接口
interface Point {
x: number;
y: number;
}
function printCoord(pt: Point) {
console.log("坐标x的值是: " + pt.x)
console.log("坐标y的值是: " + pt.y);
}
printCoord({ x: 100, y: 100 })
可以用接口来定义对象的类型, 几乎所有可以通过interface来定义的类型都可以用type来定义
类型别名type 和接口interface之间的区别:
- 扩展接口: 通过extends
// 扩展接口
interface Animal {
name: string
}
interface Bear extends Animal {
honey: boolean
}
const bear: Bear = {
name: 'winie',
honey: true
}
console.log(bear.name, bear.honey)
扩展类型别名: 通过 &
type Animal = {
name: string
}
type Bear = Animal & {
honey: boolean
}
const bear: Bear = {
name: "winie",
honey: true
}
-
向现有的类型添加新字段
接口: 定义相同的接口, 其字段会合并.
interface MyWindow {
count: number
}
interface MyWindow {
title: string
}
const w: MyWindow = {
title: 'hello ts',
count: 10
}
类型别名: 类型别名创建的类型创建后是不能添加新字段的
3.10 类型断言 as
const myCanvas = document.getElementById("main_canvas") // 返回某种类型的HTMLElement
// 可以使用类型断言来指定
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement
const myCanvas = <HTMLCanvasElement>document.getElementById()
类型注释与类型断言一样, 类型断言由编译器来删除, 不会影响代码的运行时行为, 也就是因为类型断言在编译时被删除, 所以没有与类型断言相关联的运行时检查.
const x = ('hello' as unknown) as number
如上代码可以在我们不知道某些代码是什么类型的时候断言为一个差不多的类型.
3.11 文字类型
除了一般类型string
和number
, 还可以在类型位置引用特定的字符串和数字.
一种方法是考虑js如何以不同的方式声明变量. var
和let
两者都允许更改变量中保存的内容, const
不允许, 这反映在TS如何为文字创建类型上
let testString = "Hello World";
testString = "Olá Mundo";
// 'testString'可以表示任何可能的字符串,那TypeScript是如何在类型系统中描述它的
testString;
const constantString = "Hello World";
// 因为'constantString'只能表示1个可能的字符串,所以具有文本类型表示
constantString;
就其本身而言, 文字类型不是很有价值
let x: "hello" = "hello";
// 正确
x = "hello"
// 错误
x = "howdy"
拥有一个只能由一个值的变量并没有多大用处!
但是通过将文字组合成联合,你可以表达一个更有用的概念——例如,只接受一组特定已知值的函数:
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");
数字文字类型的工作方式相同:
function compare(a: string, b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}
也可以将这些与非文字类型结合使用:
interface Options {
width: number;
}
function configure(x: Options | "auto") {
// ...
}
configure({ width: 100 });
configure("auto");
configure("automatic");
还有一种文字类型:布尔文字。只有两种布尔文字类型,它们是类型 true
和 false
。类型 boolean 本身实际上只是联合类型 union
的别名 true | false
。
文字推理
当你使用对象初始化变量时,TypeScript 假定该对象的属性稍后可能会更改值。例如,如果你写了这样的代码:
const obj = { counter: 0};
if(someCondtion) {
obj.counter = 1
}
TypeScript 不假定先前具有的字段值 0
,后又分配 1
是错误的。另一种说法是 obj.counter
必须有 number
属性, 而非是 0
,因为类型用于确定读取和写入行为。
这同样适合用于字符串:
function handleRequest(url: string, method: 'GET' | 'POST' | 'GUESS') {
// ...
}
const req = { url: 'https://example.com', method: 'GET' };
handleRequest(req.url, req.method);
在上面的例子 req.method
中推断是 string
,不是 "GET"
。因为代码可以在创建 req
和调用之间进行评估,TypeScript 认为这段代码有错误。
有两种方法可以解决这个问题:
- 可以通过在任一位置添加类型断言来更改推理:
// 方案 1:
const req = { url: "https://example.com", method: "GET" as "GET" };
// 方案 2
handleRequest(req.url, req.method as "GET");
方案1表示“我打算 req.method 始终拥有文字类型"GET"
”,从而防止之后可能分配"GUESS"
给该字段。
方案 2 的意思是“我知道其他原因req.method
具有"GET"
值”。
- 可以使用
as const
将整个对象转换为类型文字
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);
该as const
后缀就像const
定义,确保所有属性分配的文本类型,而不是一个更一般的string
或 number
。
3.12 null
和undefined
JavaScript 有两个原始值用于表示不存在或未初始化的值: null
和 undefined
.
TypeScript有两个对应的同名类型。这些类型的行为取决于您是否在tsconfig.json
设置strictNullChecks
选择。
-
strictNullChecks
关闭使用false,仍然可以正常访问的值,并且可以将值分配给任何类型的属性。这类似于没有空检查的语言 (例如 C#、Java)的行为方式。缺乏对这些值的检查往往是错误的主要来源;如果在他们的代码库中这样做可行,我们总是建议大家打开。
-
strictNullChecks
开启使用true,你需要在对该值使用方法或属性之前测试这些值。就像在使用可选属性之前检查一样,我们可以使用缩小来检查可能的值:
function doSomething(x: string | null) {
if (x === null) {
// 做一些事
} else {
console.log("Hello, " + x.toUpperCase());
}
}
-
非空断言运算符(
!
后缀)
TypeScript 也有一种特殊的语法 null
, undefined
, 可以在不进行任何显式检查的情况下,从类型中移除和移除类型。 !
在任何表达式之后写入实际上是一种类型断言,即该值不是 null
or undefined
:
使用?
可以指定对象中某个参数可以选择传入或者不传入, 不传入其值就是undefined.
function liveDangerously(x?: number | null) {
// 正确
console.log(x!.toFixed());
}
就像其他类型断言一样,这不会更改代码的运行时行为,因此仅 !
当你知道该值不能是 null
或 undefined
时使用才是重要的。
3.13 枚举
枚举是 TypeScript 添加到 JavaScript 的一项功能,它允许描述一个值,该值可能是一组可能的命名常量之一。与大多数 TypeScript 功能不同,这不是JavaScript 的类型级别的添加,而是添加到语言和运行时的内容。因此,你确定你确实需要枚举在做些事情,否则请不要使用。可以在Enum参考页中阅读有关枚举的更多信息。
// ts源码
enum Direction {
Up = 1,
Down,
Left,
Right,
}
console.log(Direction.Up) // 1
// 编译后的js代码
"use strict";
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 1] = "Up";
Direction[Direction["Down"] = 2] = "Down";
Direction[Direction["Left"] = 3] = "Left";
Direction[Direction["Right"] = 4] = "Right";
})(Direction || (Direction = {}));
console.log(Direction.Up);
3.14 不太常见的原语
值得一提的是JavaScript中一些较新的原语, 它们在 TypeScript 类型系统中也实现了。我们先简单的看两个例子:
bigint
从 ES2020(ES11) 开始,JavaScript 中有一个用于非常大的整数的原语BigInt
:
// 通过bigint函数创建bigint
const oneHundred: bigint = BigInt(100);
// 通过文本语法创建BigInt
const anotherHundred: bigint = 100n;
你可以在TypeScript 3.2发行说明中了解有关 BigInt 的更多信息。
symbol
JavaScript 中有一个原语 Symbol()
,用于通过函数创建全局唯一引用:
const firstName = Symbol("name");
const secondName = Symbol("name");
if (firstName === secondName) {
// 这里的代码不可能执行
}
此条件将始终返回 false
,因为类型typeof firstName
和typeof secondName
没有重叠。
TypeScript学习第四章: 类型缩小
假设我们有一个名为padLeft的函数:
function padLeft(padding: number | string, input: string): string {
throw new Error("尚未实现!");
}
我们来扩充一下功能: 如果padding
是number
, 它会将其视为我们将要添加到input
的空格数; 如果padding
是string
, 它只在input上做padding
. 让我们尝试实现:
function padLeft(padding: number | string, input: string): string {
return new Array(padding + 1).join(" ") + input;
}
这样的话, 我们在padding + 1处会遇到错误. TS警告我们, 运算符+不能应用于类型number | string
和 string
, 这个逻辑是对的, 因为我们没有明确检查padding是否为number
, 也没有处理它是string
的情况, 所以我们我们这样做:
function padLeft(padding: number | string, input: string): string {
if (typeof padding === "number") {
return new Array(padding + 1).join(" ") + input;
}
return padding + input;
}
如果这大部分看起来像无趣的JavaScript代码,这也算是重点吧。除了我们设置的注解之外,这段 TypeScript代码看起来就像JavaScript。
我们的想法是,TypeScript的类型系统旨在使编写典型的 JavaScript代码变得尽可能容易,而不需要弯腰去获得类型安全。
虽然看起来不多,但实际上有很多价值在这里。就像TypeScript使用静态类型分析运行时的值一样,它在JavaScript的运行时控制流构造上叠加了类型分析,如if/else、条件三元组、循环、真实性检查等,这些都会影响到这些类型。
在我们的if检查中,TypeScript看到typeof padding ==="number"
,并将其理解为一种特殊形式的代码,称为类型保护。TypeScript遵循我们的程序可能采取的执行路径,以分析一个值在特定位置的最具体的可能类型。它查看这些特殊的检查(称为类型防护)和赋值,将类型细化为比声明的更具体的类型的过程被称为类型缩小。在许多编辑器中,我们可以观察这些类型的变化,我们甚至会在我们的例子中这样做。
TypeScript 可以理解几种不同的缩小结构.
4.1 typeof
类型守卫
正如我们所见, Js支持typeof
运算符, 它可以提供有关我们在运行时拥有的值类型的非常基本的信息.
TS期望它返回一组特定的字符串:
"string"
"number"
"bigint"
"boolean"
"symbol"
"undefined"
"object"
"function"
就像我们刚才在padLeft
中看到的那样, 这个运算符经常出现在许多JavaScript库中, TS可以理解为, 它缩小在不同分支中的类型.
在TS中, 检查typeof
的返回值是一种类型保护. 因为TS对typeof
操作进行编码, 从而返回不同的值, 所以它知道对JS做了什么. 例如, 请注意上面的列表中, typeof
不返回null
.
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// 做点事
}
}
在 printAll
函数中,我们尝试检查 strs
是否为对象,来代替检查它是否为数组类型(现在可能是强调数组是 JavaScript 中的对象类型的好时机)。但事实证明,在 JavaScript 中, typeof null
实际上也是 "object"
! 这是历史上的不幸事故之一。
有足够经验的用户可能不会感到惊讶,但并不是每个人都在 JavaScript 中遇到过这种情况;幸运的是, ts 让我们知道, strs
只缩小到 string[] | null
,而不仅仅是 string[].
这可能是我们所谓的“真实性”检查的一个很好的过渡。
4.2 真值缩小
真值检查是我们在JS中经常做的一件事. 在JS中, 我们可以在条件 &&
||
if
语句布尔否定(!)等中使用任何表达式.
例如, if
语句不希望它们的条件总是具有类型boolean
function getUserOnlineMessage(numUserOnline: number) {
if(numUserOnline) {
return `现在共有 ${numUserOnline} 人在线!`
}
return "现在没有人在线:("
}
在JS总, if条件语句, 首先把他们的条件强制转化为boolean以使其有意义, 然后根据结果是true还是false来选择他们的分支. 像下面这些值都强制转换为false:
- 0
- NaN
- "" (空字符串)
- On (bigint 0的版本)
- null
- undefined
其他值被强制转化为true
. 你始终可以在Boolean
函数中运行值获得boolean
, 或使用较短的双布尔否定将值强制转换为boolean
.(后者的优点是ts推断出一个狭窄的文字布尔类型true, 而将第一个推断为boolean
类型)
// 这两个结果都返回 true
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true, value: true
利用这个特性, 我们可以防范诸如null
或undefined
之类的值时. 例如, 让我们尝试将它用于我们的printAll
函数.
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
我们通过检查strs
是否为真, 消除了上述错误. 这可以防止我们在运行代码的时候出现一些错误, 例如:
TypeError: null is not iterable
但请记住, 对原语的真值检查通常容易出错. 例如, 考虑改写printAll
:
function printAll(strs: string | string[] | null) {
// !!!!!!!!!!!!!!!!
// 别这样!
// 原因在下边
// !!!!!!!!!!!!!!!!
if (strs) {
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
}
我们将整个函数体包裹在一个真值检查中, 但是这有一个小小的缺点: 我们可能不再正确处理空字符串
的情况.
TS在这里根本不会报错, 如果你不熟悉JS, 这是值得注意的. TS通常可以帮你及早发现错误, 但是如果你选择对某个值不做任何处理, 那么它可以做的就只有这么多, 而不会考虑过多逻辑方面的问题, 如果需要, 你可以确保linter(程序规范性)处理此类情况.
关于通过真实性缩小范围的最后一点,是通过布尔否定 !
把逻辑从否定分支中过滤掉。
function multiplyAll(
values: number[] | undefined,
factor: number
): number[] | undefined {
if (!values) {
return values;
} else {
return values.map((x) => x * factor);
}
}
4.3 等值缩小
ts也使用分支语句做===
!==
==
和 !=
等值检查, 来实现类型缩小. 例如:
function example(x: string | number, y: string | boolean) {
if (x === y) {
// 现在可以在x,y上调用字符串类型的方法了
x.toUpperCase();
y.toLowerCase();
} else {
console.log(x);
console.log(y);
}
}
当我们在上面的示例中检查 x 和 y 是否相等时,TypeScript知道它们的类型也必须相等。由于 string 是 x 和 y 都可以采用的唯一常见类型,因此TypeScript 知道 x 、 y 如果都是 string
,则程序走第一个分支中 。
检查特定的字面量值(而不是变量)也有效。在我们关于真值缩小的部分中,我们编写了一个 printAll 容易出错的函数,因为它没有正确处理空字符串。相反,我们可以做一个特定的检查来阻止 null ,并且 TypeScript 仍然正确地从 strs 里移除 null 。
function printAll(strs: string | string[] | null) {
if (strs !== null) {
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
}
JavaScript 更宽松的相等性检查 ==
和 !=
,也能被正确缩小。如果你不熟悉,如何检查某个变量是否 == null
,因为有时不仅要检查它是否是特定的值 null
,还要检查它是否可能是 undefined
。这同样适用 于 == undefined
:它检查一个值是否为 null 或 undefined 。现在你只需要这个 ==
和 !=
就可以搞定了。
interface Container {
value: number | null | undefined;
}
function multiplyValue(container: Container, factor: number) {
// 从类型中排除了undefined 和 null
if (container.value != null) {
console.log(container.value);
// 现在我们可以安全地乘以“container.value”了
container.value *= factor;
}
}
console.log(multiplyValue({value: 5}, 5))
console.log(multiplyValue({value: null}, 5))
console.log(multiplyValue({value: undefined}, 5))
console.log(multiplyValue({value: '5'}, 5))
4.4 in
操作符缩小
JavaScript 有一个运算符,用于确定对象是否具有某个名称的属性: in
运算符。TypeScript 考虑到了这 一点,以此来缩小潜在类型的范围。 例如,使用代码: "value" in x
。这里的 "value"
是字符串string, x
是联合类型。值为“true”的分支缩小,需要 x 具有可选或必需属性的类型的值;值为 “false”
的分支缩小,需要具有可选或缺失属性的类型的值。
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
}
return animal.fly();
}
另外,可选属性还将存在于缩小的两侧,例如,人类可以游泳和飞行(使用正确的设备),因此应该出 现在 in
检查的两侧:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };
function move(animal: Fish | Bird | Human) {
if ("swim" in animal) {
// animal: Fish | Human
animal;
} else {
// animal: Bird | Human
animal;
}
}
4.5 instanceof
操作符缩小
JS有一个运算符instanceof
检查一个值是否是另一个值的“实例”。更具体地,在JavaScript 中 x instanceof Foo
检查 x
的原型链是否含有 Foo.prototype
。虽然我们不会在这里深入探讨,当 我们进入 类(class)
学习时,你会看到更多这样的内容,它们大多数可以使用 new
关键字实例化。 正如你可能已经猜到的那样, instanceof
也是一个类型保护,TypeScript 在由 instanceof
保护的分支中实现缩小。
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
} else {
console.log(x.toUpperCase());
}
}
logValue(new Date()) // Mon, 15 Nov 2021 22:34:37 GMT
logValue('hello ts') // HELLO TS
4.6 分配缩小
正如我们之前所提到的, 当我们为任何变量赋值时, TS会检查赋值的右侧并适当缩小左侧.
// let x: string | number
let x = Math.random() < 0.5 ? 10 : "hello world!";
x = 1;
// let x: number
console.log(x);
x = "goodbye!";
// let x: string
console.log(x);
请注意,这些分配中的每一个都是有效的。即使在我们第一次赋值后观察到的类型 x 更改为 number
, 我们仍然可以将 string
赋值给 x 。这是因为声明类型 x 开始是 string | number
。
如果我们分配了一个 boolean
给 x ,我们就会看到一个错误,因为它不是声明类型的一部分。
let x = Math.random() < 0.5 ? 10 : "hello world!";
// let x: string | number
x = 1;
// let x: number
console.log(x);
// 出错了~!
x = true
// let x: string | number
console.log(x);
4.7 控制流分析
到目前为止, 我们已经通过一些基本实例来说明TS如何在特定分支中缩小范围. 但是除了从每个变量中走出来, 并在if
、while
条件等中寻找类型保护之外, 还有更多的事情要做, 比如:
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return new Array(padding + 1).join(" ") + input;
}
return padding + input;
}
padLeft从其第一个if块中返回. TS能够分析这段代码,并看到在padding是数字的情况下, 主体的其余部分( return padding + input;
)是不可达的。因此,它能够将数字从 padding
的类型中移除(从string|number缩小到string),用于该函数的其余部分。
这种基于可达性的代码分析被称为控制流分析,TypeScript使用这种流分析来缩小类型,因为它遇到了 类型守卫和赋值。当一个变
请发表评论