TypeScript基础篇学习笔记(千锋-陆神)持续更新中...

一、TypeScript介绍

1.1 什么是TypeScript

        TypeScript 是由微软 2012年推出的,自由和开源的编程语言。这门语言在业界已经相当的流行。我们熟知的Vue, React, Angular 这些广泛应用的前端框架,都在使用 TypeScript进行开发,鸿蒙系统3.0也重点推荐使用JS开发应用,在最新推出的开发框架ArkUI, 也全面拥抱TS了。
TypeScript是JavaScript的超集,正式因为它是建立在JavaScript上的一门语言,TypeScript把其他语言的一些精妙的语法带入到JavaScript之中,从而把JS带到一个新的高度,你可以在TS中使用JS之外的扩展语法,同时借助TS对面向对象和静态类型的良好支持,你可以编写更健壮,更可维护的大型项目。

1.2 JS ,ES ,TS的关系

        1995年:JavaScript 当时的网景公司正凭借其Navigator浏览器成为Web时代开启时最著名的第一代互联网公司。 由于网景公司希望能在静态HTML页面上添加一些动态效果,于是 Brendan Eich 在两周之内设计出了 JavaScript语言。 为什么起名叫JavaScript?原因是当时Java语言非常红火,所以网景公司希望借Java的名气来推广,但事实上JavaScript除了语法上有点像Java,其他部分基本上没啥关系。

        1997年:ECMAScript 因为网景开发了JavaScript,一年后微软又模仿JavaScript开发了JScript,为了让JavaScript成为全球标准,几个公司联合ECMA(European Computer Manufacturers Association)(欧洲计算机制造商协 会)组织制定了JavaScript 语言的标准,被称为ECMAScript标准。

       TypeScript 是 JavaScript 的超集,即包含JavaScript 的所有元素,能运行JavaScript 的代码,并扩展了 JavaScript 的语法。相比于JavaScript ,它还增加了静态类型、类、模块、接口和类型注解方面的功能, 更易于大项目的开发。

       总而言之,ECMAScript是JavaScript的标准,TypeScript是JavaScript 的超集。

二、TypeScript入门

2.1 静态类型检查

const message = 'Hello World'

message()

静态类型检查捕获到的错误:

This expression is not callable. Type 'String' has no call signatures.(这个表达式不可以调用,类型 'String' 没有 call 这个方法)

2.2 非异常故障

在JavaScript中调用对象中不存在的属性或方法JavaScript会返回undefined

const user = {
    name: "小新",
    age: 18,
};
user.location

在TypeScript中会产生错误:Property 'location' does not exist on type '{ name: string; age: number; }'(属性 'location' 在对象中不存在)

TypeScript 不仅可以捕获程序中的错误而且还可以检查拼写错误。例如:

//错误示例
const message = 'Hello World'

message.toLocaleLowerCaes()

TypeScript 捕获到的错误:Property 'toLocaleLowerCaes' does not exist on type '"Hello World"'. Did you mean 'toLocaleLowerCase'?(属性'toLocaleLowerCaes' 不存在,你是不是想要调用'toLocaleLowerCase'?)

TypeScript 还可以检查未调用的函数的检查的一些非异常故障。例如:

//错误示例
function fn() {
    return Math.random < 18
}

TypeScript 捕获到的错误:Operator '<' cannot be applied to types '() => number' and 'number'. (只需要给random加上小括号就没有异常了)

TypeScript还可以检查逻辑错误。例如:

const value = Math.random() < 18 ? 'small':'big'

if(value !== 'small') {

} else if(value === 'big') {
    
}

TypeScript 捕获到的错误:This condition will always return 'false' since the types '"small"' and '"big"' have no overlap.

2.3 使用工具

1、安装VSCode

https://code.visualstudio.com/Download

2、安装Node.js

https://nodejs.org/en/download/

3、安装TypeScript编译器

npm install typescript –g

2.4 tsc编译器

//hello.ts
function greet(preson,date) {
    console.log(`Hello ${preson} today is ${date}`);
}

greet('小新','2022-05-08')

      因为浏览器无法识别ts代码,因此需要在终端使用命令 tsc hello.ts(文件名),将ts代码编译为js代码:

//hello.js
function greet(preson, date) {
    console.log("Hello ".concat(preson, " today is ").concat(date));
}
greet('小新', '2022-05-08');

在终端使用命令 node hello.js(文件名)  执行后的结果为:Hello 小新 today is 2022年5月8日

2.5 优化编译

1、解决TSJS冲突问题

当TS编译成JS文件后,当出现函数名或变量名相同时,会提示重复定义的问题,解决该问题可以使用命令 tsc --init  生成ts配置文件,在上述代码中greet()函数的两个参数并未指定类型,在终端会提示错误,可在tsconfig.json配置文件中将 "strict" 改为false(关闭严格模式---不建议关闭),为greet()函数的两个参数指定类型即可

2、自动编译

当ts代码发生变化后并不会自动编译为js代码,可使用终端命令 tsc --watch ,ts代码发生变化并保存后会将ts代码自动编译为js代码

3、发出错误

当ts代码发生错误后仍然可以将ts文件编译为js文件,解决该问题可以使用命令

tsc --noEmitOnError --watch,ts代码没有错误后自动编译为js代码

2.6 显式类型

上述代码中greet()函数 我们还没有告诉 typescript person 或者 date 是什么类型。

//为preson , date指定类型
function greet(preson:string,date:Date) {
    console.log(`Hello ${preson} today is ${date}`);
}

greet('野原新之助',new Date())

我们并不是必须要给变量指定类型。在许多情况下,TypeScript 可以为我们自动推断(或“找出”)类型,即使我们忽略定义这些类型。比如:

let message = 'hello xiaoxin'

message = 'hello world'

message = 18

变量message最初是字符串,当我们将数值18赋值给message时,终端会提示 "message": "不能将类型“number”分配给类型“string”。"

2.6 降级编译

通常ts是默认将js编译为符合es6的语法规范的,如需改为其他es版本,可将tsconfig.json配置文件下的"target"改为es5,这样ts代码编译出来的js就符合es5的规范

"target": "es5"

2.7 严格模式

不同的用户使用 TypeScript 在类型检查器中,希望检查的严格程度不同。有些人正在寻找更宽松的验证 体验,它可以帮助仅验证其程序的某些部分,并且仍然拥有不错的工具。这是 TypeScript 的默认体验, 其中类型是可选的,推理采用最宽松的类型,并且不检查潜在的 null / undefined 值,就像 tsc 面对错 误时如何编译生成JS文件一样。

typescript 有三个严格检查标记,strict,noImplicitAny , strictNullChecks

当未给参数指定类型时,会提示具有隐式的'any'类型,例如:

function greet(preson,date) {
    console.log(`Hello ${preson} today is ${date}`);
}

greet('野原新之助',new Date())

error:参数"preson"隐式具有"any"类型。

error:参数"date"隐式具有"any"类型。

 可将tsconfig.json配置文件下的"strict"改为"false"即可关闭严格模式(一般不建议关闭)

三、常用类型

3.0 TypeScript配置文件

命令 tsc --init 生成ts配置文件 , 在配置文件中指定源文件中的根文件夹("rootDir": "./src")  ,为编译后的js文件指定输出文件夹  ("outDir": "./dist")

{
"compilerOptions": {
/* Language and Environment */
"target": "es6",
/* Modules */
"rootDir": "./src",
/* Emit */
"outDir": "./dist",
/* Type Checking */
"strict": true,
}
}

3.1 基元类型string ,number , 和boolean

JavaScript有三个非常常用的原语: string , number ,和 boolean 。每个在 TypeScript 中都有对应 的类型。这些名称与我们在 JavaScript 应用 typeof 返回的类型的名称相同: string 表示字符串值,如 "Hello, TypeScript"  ,number 表示数字值,如 18 。JavaScript 没有一个特殊的整数运行时值,所以没有等价于 int 或 float 类型, 一切都只是 number,   boolean 只有两个值 true 和 false

let str: string = 'hello typescript'
let num: number = 18
let bool: boolean = true

3.2 数组

数组是指定形如 [1, 2, 3] 数据,可以使用语法 number[ ] 来定义数组类型为数值型; 此语法适用于任何类型(例如 string[ ] ,字符串数组等)。 也可以写成 Array

let arr1: number[] = [1, 2, 3]

let arr2: Array = [1, 2, 3]

3.3 any

TypeScript 还有一个特殊类型 any ,当不希望某个特定值导致类型检查错误时,可以使用它。 当一个值的类型是 any 时,可以访问它的任何属性,将它分配给任何类型的值,或者几乎任何其他语法 上的东西都合法的。

let obj: any = { x: 0 };
// 以下代码行都不会抛出编译器错误。
// 使用'any'将禁用所有的类型检查
obj.fn();
obj();
obj.age= 18;
obj = "hello";
const n: number = obj

3.4 变量上的类型注释

当使用 const , var , 或声明变量时 let ,可以选择添加类型注释来显式指定变量的类型:

let name: string = "peiqi";

在大多数情况下,这不是必需的。TypeScript 会尝试自动推断代码中的类型。例如,变量的类型是根据其初始化器的类型推断出来的。

// 不需要类型定义“name”  typescript自动推断为类型“string”
let name = "peiqi";

3.5 函数

函数是在 JavaScript 中传递数据的主要方式。TypeScript 允许您指定函数的输入和输出值的类型。

1 、参数类型注释 :声明函数时,可以在每个参数后添加类型注解,以声明函数接受的参数类型。参数类型注释位于参数名称之后:

// 参数类型定义
function greet(name: string) {
console.log("Hello, " + name + "!");
}

2 、返回类型注释 :还可以添加返回类型注释。返回类型注释出现在参数列表之后:

function getNumber(): number {
return 18;
}

返回类型注释与变量类型注释非常相似,通常不需要返回类型注释,因为 TypeScript 会根据其 return 语句推断函数 的返回类型。

3 、匿名函数: 匿名函数与函数声明有点不同。当一个函数出现在 TypeScript 可以确定它将如何被调用的地方时,该函数的参数会自动指定类型

// 这里没有类型注释,但是TypeScript可以判断类型
const names = ["小新", "佩奇", "乔治"];

// 函数上下文类型
names.forEach(function (item) {
console.log(s.toUppercase());
});

// 上下文类型也适用于箭头函数
names.forEach((item) => {
console.log(s.toUppercase());
})

3.6 对象类型

除了 string , number , boolean 类型(又称基元类型)外,你将遇到的最常见的类型是对象类型。 这指的是任何带有属性的 JavaScript 值,几乎是所有属性!要定义对象类型,我们只需列出其属性及其 类型。

例如,这是一个接受点状对象的函数:

// 参数的类型注释是对象类型
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 }) {
// ...
}
// 两种传递参数都可以
printName({ first: "小新" });

printName({ first: "小新", last: "图图" });

在 JavaScript 中,如果访问一个不存在的属性,将获得值 undefined 。因此,当读取可选属性时,使用它之前必须用 undefined 进行检查:

function printName(obj: { first: string; last?: string }) {
// 错误 - 'obj.last' 可能不存在!
console.log(obj.last.toUpperCase());

// 可以这样
if (obj.last !== undefined) {
console.log(obj.last.toUpperCase());
}

// 使用现代JavaScript语法的安全替代方案:
console.log(obj.last?.toUpperCase());
}

3.7 联合类型

1、定义联合类型: 第一种组合类型的方法是联合类型。联合类型是由两个或多个其他类型组成的类型,表示可能是这些类型中的任何一种的值。联合类型使用符号" | "

编写一个可以对字符串或数字进行操作的函数:

function id(id: number | string) {
console.log("Your ID is: " + id);
}
// 正确
id(101);
// 正确
id("202");
// 错误
id({ myID: 22342 });

2、使用联合类型 :  提供匹配联合类型的值很容易- 只需提供匹配任何联合成员的类型。 如果联合的每个成员都有效,TypeScript 将只允许使用联合做一些事情。例如,如果联合类型 string | number ,则不能只使用一种类型的操作,比如 string :

function id(id: number | string) {
//错误 : number 类型不存在 toUpperCase()方法
console.log(id.toUpperCase());
}

解决方案是用代码缩小联合,就像在没有类型注释的 JavaScript 中一样。 当 TypeScript 可以根据代码 结构为值推断出更具体的类型时,就会发生缩小。

例如,TypeScript 知道只有一个 string 值才会有一个 typeof 值 "string" :

function id(id: number | string) {
if (typeof id === "string") {
// 在此分支中,id的类型为“string”
console.log(id.toUpperCase());
} else {
// 此处,id的类型为“number”
console.log(id);
}
}

另一个例子是使用如下函数 Array.isArray:

function welcomePeople(x: string[] | string) {
if (Array.isArray(x)) {
// 此处: 'x' 的类型是 'string[]'
console.log("Hello, " + x.join(" and "));
} else {
// 此处: 'x' 的类型是 'string'
console.log("Welcome lone traveler " + x);
}
}

当所有类型成员都有一些共同点。例如,数组和字符串都有一个 slice 方法。如果联合类型中的每个成员都有一个共同的属性,则可以使用该属性而不会缩小范围:

// 返回类型自动推断为 number[] | string
function getFirstThree(x: number[] | string) {
return x.slice(0, 3);
}

3.8 类型别名

在通过直接在类型注释中编写对象类型和联合类型来使用它们。这很方便,但是想要多次使用同一个类型,并用一个名称来引用。 一个类型别名正是一个名称为任何类型的定义。类型别名的语法是:

type Point = {
x: number;
y: number;
};
// 与前面的示例完全相同
function printCoord(pt: Point) {
console.log("坐标x的值是: " + pt.x);
console.log("坐标y的值是: " + pt.y);
}
printCoord({ x: 100, y: 100 });

实际上,可以使用类型别名为任何类型命名,而不仅仅是对象类型。例如,类型别名可以命名联合类 型:

type ID = number | string;

请注意,别名只是别名 - 不能使用类型别名来创建相同类型的不同“版本”。当使用别名时,就像编 写了别名类型一样。换句话说,这段代码可能看起来不合法,但根据 TypeScript 是可以的,因为这两种 类型都是同一类型的别名:

type UserInputSanitizedString = string;
function sanitizeInput(str: string): UserInputSanitizedString {
return str.slice(0, 2)
}
// 创建经过 sanitize 的输入
let userInput = sanitizeInput('hello');
// 但仍可以使用字符串重新分配值
userInput = "new input";

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 ,关键区别在于扩展新类型的方式不同:接口可使用关键字extends扩展类型,例如:

// 扩展接口
interface Animal {
name: string
}
interface Bear extends Animal {
honey: boolean
}
const bear: Bear = {
name: 'peiqi',
honey: true
}
bear.name
bear.honey

通过类型别名扩展新类型:使用符号"&" 扩展类型

// 通过交叉点扩展类型
type Animal = {
name: string
}
type Bear = Animal & {
honey: boolean
}
const bear: Bear = {
name: 'peiqi',
honey: true
}
bear.name;
bear.honey

向现有接口添加新字段:

interface MyWindow {
title: string
}
interface MyWindow {
count: number
}
const w: MyWindow = {
title: 'hello ts',
count: 100
}

类型创建后不可更改:

type MyWindow = {
title: string
}
//错误 : 重复定义MyWindow
type MyWindow = {
count: number
}

类型与接口总结:类型别名可能不参与声明合并,但接口可以。 接口只能用于声明对象的形状,不能重命名基元。 接口名称将始终以其原始形式出现在错误消息中,但仅当它们按名称使用时。

3.10 类型断言

当获得有关 TypeScript 不知道的值类型的信息。 例如,如果正在使document.getElementById ,TypeScript 只知道这将返回某种类型的 HTMLElement ,但你可能知道你的页面将始终具有 HTMLCanvasElement 给定 ID 的值 。 在这种情况下,可以使用类型断言来指定更具体的类型:

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;

与类型注释一样,类型断言由编译器删除,不会影响代码的运行时行为。 还可以使用尖括号语法(除非代码在 .tsx 文件中),它是等效的:

const myCanvas = document.getElementById("main_canvas");

TypeScript 只允许类型断言转换为更具体或不太具体的类型版本。此规则可防止“不可能”的强制,例如:

const x = "hello" as number

将类型 string 转换为类型 number 可能是错误的,因为两种类型都没有充分重叠。如果这是有意的,请 先将表达式转换为 any 或 unknown ( unknown ,我们将在后面介绍),然后是所需的类型:

const x = ("hello" as unknown) as number

3.11 文字类型

除了一般类型 string 和 number ,可以在类型位置引用特定的字符串和数字。 一种方法是考虑 JavaScript 如何以不同的方式声明变量。 var 而 let 两者都允许更改变量中保存的内 容, const 不允许,这反映在 TypeScript 如何为文字创建类型上

let testString = "Hello World";
testString = "Hello Peiqi";
//'testString'可以表示任何可能的字符串,那
//TypeScript是如何在类型系统中描述它的
testString;
const constantString = "Hello World";
//因为'constantString'只能表示1个可能的字符串,所以
//具有文本类型表示
constantString;

就其本身而言,文字类型并不是很有价值:

let x: "hello" = "hello";
// 正确
x = "hello";
// 错误
x = "peiqi"

拥有一个只能有一个值的变量并没有多大用处! 但是通过将文字组合成联合,你可以表达一个更有用的概念——例如,只接受一组特定已知值的函数:

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 (someCondition) {
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. 可以通过在任一位置添加类型断言来更改推理:

// 方案 1:
const req = { url: "https://example.com", method: "GET" as "GET" };
// 方案 2
handleRequest(req.url, req.method as "GET");

2. 可以使用 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 有两个对应的同名类型。这些类型的行为取决于是否设置 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 :

function liveDangerously(x?: number | null) {
// 正确
console.log(x!.toFixed());
}

3.13 枚举

枚举是 TypeScript 添加到 JavaScript 的一项功能,它允许描述一个值,该值可能是一组可能的命名常量 之一。与大多数 TypeScript 功能不同,这不是JavaScript 的类型级别的添加,而是添加到语言和运行时的内容。

ts源码:

// ts源码
enum Direction {
Up = 1,
Down,
Left,
Right,
}
console.log(Direction.Up) // 输出1

编译后的js代码:

// 编译后的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 不太常见的原语

bigint 从 ES2020 开始,JavaScript 中有一个用于非常大的整数的原语 BigInt :

// 通过bigint函数创建bigint
const oneHundred: bigint = BigInt(100);
// 通过文本语法创建BigInt
const anotherHundred: bigint = 100n;

symbol JavaScript 中有一个原语 Symbol() ,用于通过函数创建全局唯一引用:

const firstName = Symbol("name");
const secondName = Symbol("name");
if (firstName === secondName) {
// 这里的代码不可能执行
}

四、类型缩小

假设有一个名为 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) {
return new Array(padding + 1).join(" ") + input;
}

在 padding + 1 处遇到错误。TypeScript 警告,运算符 + 不能应用于类型 string | number 和 number ,这是对的。换句话说,没有明确检查 padding 是否为 number ,也没有处理 它是 string 的情况,所以这样做:

function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
return new Array(padding + 1).join(" ") + input;
}
return padding + input;
}

4.1 typeof类型守卫

JavaScript 支持一个 typeof 运算符,它可以提供有关我们在运行时拥有的值类型的非常 基本的信息。TypeScript 期望它返回一组特定的字符串:"string" "number" "bigint" "boolean" "symbol" "undefined" "object" "function"

在 TypeScript 中,检查 typeof 的返回值是一种类型保护

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" !

4.2 真值缩小

真值检查是在 JavaScript 中经常做的一件事。在 JavaScript 中,我们可以在条件、 && 、 || 、 if 语句、布尔否定 ( ! ) 等中使用任何表达式。

例如, if 语句不希望它们的条件总是具有类型 boolean

function getUsersOnlineMessage(numUsersOnline: number) {
if (numUsersOnline) {
return `现在共有 ${numUsersOnline} 人在线!`;
}
return "现在没有人在线. :(";
}

在 JavaScript 中,像这样的 if 条件语句,首先将它们的条件“强制”转化为 boolean 以使其有意义,然 后根据结果是 true 还是 false 来选择它们的分支。像这面这些值: 0   NaN    "" (空字符串)  null undefined 以上所有值强制都转换为 false ,其他值被强制转化为 true 。始终可以在 Boolean 函数中运行值获 得 boolean ,或使用较短的双布尔否定将值强制转换为 boolean 。(后者的优点是 TypeScript 推断出 一个狭窄的文字布尔类型 true ,而将第一个推断为 boolean 类型。)

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);
}
}

关于通过真实性缩小范围的最后一点,是通过布尔否定 ! 把逻辑从否定分支中过滤掉

function multiplyAll(
values: number[] | undefined,
factor: number
): number[] | undefined {
if (!values) {
return values;
} else {
return values.map((x) => x * factor);
}
}

4.3 等值缩小

typescript 也使用分支语句做 === , !== , == ,和 != 等值检查,来实现类型缩小。

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;
}
}

4.4 in操作符缩小

JavaScript 有一个运算符,用于确定对象是否具有某个名称的属性: in 运算符。TypeScript 考虑到了这 一点,以此来缩小潜在类型的范围。 例如,使用代码: "value" in x 。这里的 "value" 是字符串文字, 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操作符缩小

JavaScript 有一个运算符来 instanceof 检查一个值是否是另一个值的“实例”。更具体地,在JavaScript 中 x instanceof Foo 检查 x 的原型链是否含有 Foo.prototype 。

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 分配缩小

当为变量赋值时,TypeScript 会查看赋值的右侧并适当缩小左侧。

// 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 -该类型 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 控制流分析

到目前为止,已经通过一些基本示例来说明 TypeScript 如何在特定分支中缩小范围。但是除了从每个变量中走出来,并在 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 块中返回。TypeScript 能够分析这段代码,并看到在 padding 是数字的情况 下,主体的其余部分( return padding + input; )是不可达的。因此,它能够将数字从 padding 的 类型中移除(从字符串|数字缩小到字符串),用于该函数的其余部分。 这种基于可达性的代码分析被称为控制流分析,TypeScript使用这种流分析来缩小类型,因为它遇到了 类型守卫和赋值。当一个变量被分析时,控制流可以一次又一次地分裂和重新合并,该变量可以被观察 到在每个点上有不同的类型。

function example() {
let x: string | number | boolean;
x = Math.random() < 0.5;
// let x: boolean
console.log(x);
if (Math.random() < 0.5) {
x = "hello";
// let x: string
console.log(x);
} else {
x = 100;
// let x: number
console.log(x);
}
// let x: string | number
return x;
}
let x = example()
x = 'hello'
x = 100
x = true // error

4.8 使用类型谓词

为了定义一个用户定义的类型保护,只需要定义一个函数,其返回类型是一个类型谓词。

type Fish = {
name: string
swim: () => void
}
type Bird = {
name: string
fly: () => void
}
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined
}

在这个例子中, pet is Fish 是我们的类型谓词。谓词的形式是 parameterName is Type ,其中 parameterName 必须是当前函数签名中的参数名称。 任何时候 isFish 被调用时,如果原始类型是兼容的,TypeScript将把该变量缩小到该特定类型。

function getSmallPet(): Fish | Bird {
let fish: Fish = {
name: 'gold fish',
swim: () => {
}
}
let bird: Bird = {
name: 'sparrow',
fly: () => {
}
}
return true ? bird : fish
}
// 这里 pet 的 swim 和 fly 都可以访问了
let pet = getSmallPet()
if (isFish(pet)) {
pet.swim()
} else {
pet.fly()
}

注意,TypeScript不仅知道 pet 在 if 分支中是一条鱼;它还知道在 else 分支中,没有一条 Fish ,所以一定有一只 Bird 。 可以使用类型守卫 isFish 来过滤 Fish | Bird 的数组,获得 Fish 的数组。 

const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()]
const underWater1: Fish[] = zoo.filter(isFish)
// 或者,等同于
const underWater2: Fish[] = zoo.filter(isFish) as Fish[]
// 对于更复杂的例子,该谓词可能需要重复使用
const underWatch3: Fish[] = zoo.filter((pet): pet is Fish => {
if (pet.name === 'frog') {
return false
}
return isFish(pet)
})

 到目前为止,所看的大多数例子都是围绕着用简单的类型(如 string 、 boolean 和 number )来 缩小单个变量。虽然这很常见,但在JavaScript中,大多数时候要处理的是稍微复杂的结构。 我们将使用一个叫做 kind 的字段来告诉我们正在处理的是哪种形状。这里是定义 Shape 的第一个尝试。

interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}

注意,使用的是字符串字面类型的联合。 "circle " 和 "square " 分别告诉应该把这个形状 当作一个圆形还是方形。通过使用 "circle" | "square " 而不是 string ,可以避免拼写错误 的问题。

function handleShape(shape: Shape) {
// oops!
if (shape.kind === "rect") {
// ...
}
}

可以编写一个 getArea 函数,根据它处理的是圆形还是方形来应用正确的逻辑。我们首先尝试处 理圆形。

function getArea(shape: Shape) {
return Math.PI * shape.radius ** 2;
}

在 strictNullChecks 下,这给了一个错误——这是很恰当的,因为 radius 可能没有被定义。 但是如果对 kind 属性进行适当的检查呢?

function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
}
}

TypeScript 仍然不知道该怎么做。遇到了一个问题,即值比类型检查器知道的更多。可以尝试使用一个非空的断言 ( radius 后面的那个叹号 ! ) 来说明 radius 肯定存在。

function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius! ** 2;
}
}

但这感觉并不理想。不得不用那些非空的断言对类型检查器声明一个叹号( ! ),以说服它相信 shape.radius 是被定义的,但是如果开始移动代码,这些断言就容易出错。此外,在 strictNullChecks 之外,也可以意外地访问这些字段(因为在读取这些字段时,可选属性被认为 总是存在的)

Shape 的这种编码的问题是,类型检查器没有办法根据种类属性知道 radius 或 sideLength 是否存 在。需要把我们知道的东西传达给类型检查器。考虑到这一点,再来定义一下Shape。

interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;

在这里,正确地将 Shape 分成了两种类型,为 kind 属性设置了不同的值,但是 radius 和 sideLength 在它们各自的类型中被声明为必需的属性。

你可能感兴趣的:(typescript,javascript,前端,ecmascript)