本章节介绍的所有内容,都是可选的。也就是说,我们在 ts 中,可以选择用、或者不用。虽然知识量可能会有些多,但是都非常简单,容易理解,不要有压力。
3-1. 类型约束和编译结果对比
前言
任务列表
- 一键重命名符号
- 一键转到定义
- 类型推导
- 知道如何识别类型推导是否成功
本节课主要介绍下面两个点:
- ts 如何进行类型约束
- 源代码和编译结果的差异
notes
类型约束
一般仅会对以下内容进行约束:
- 变量
- 函数参数
- 函数返回值
通常,我们只要对函数参数和变量进行约束即可。函数的返回值会根据我们对函数参数、变量的约束,自行推导(类型推导)出来。
类型约束的语法::类型
let nickName:string;
nickName = "xxx";
// nickName = 123; // => 会报错
若我们给变量的值和类型约束所约束的类型有冲突,那么会立刻出现 ❌ 提示。
function sum(a:number, b:number):number {
return a + b;
}
function sum(a:number, b:number):number {
return a + b;
}
sum = 1; // ❎
// 如果我们采用这种方式,尝试给 sum 函数重新赋值:
// 在 js 中,这种做法是允许的
// 在 ts 中,这种做法会报错
ts 是可选的、静态的类型系统。静态的特点是,类型会在编译时就确定下来,如果在编译之前,被编译的 ts 文件出现了错误,那么将无法进行编译。
类型推导
类型推导:根据我们所写的代码,自动完成类型检查。
function sum(a:number, b:number) {
return a + b;
}
对于上面那种写法,我们可以省略掉对函数返回值的类型约束。
因为根据我们对参数 a、b 的约束,它们都是数字,返回的是它们相加的结果,必然也是一个数字。
ts 可以识别出返回值的类型必然是一个数字,所以我们可以省略对函数返回值的类型约束 => 类型推导。
我们在使用 sum 的返回值给变量 result 进行赋值的时候,也可以省略掉对变量 result 的类型约束。因为 ts 能够推导出 sum 函数的返回结果就是 number 类型,那么 result 自然也就是 number 类型啦。
// 👇🏻 两种写法是等效的
let phone:string = "11122223333";
let phone = "11122223333";
我们声明了一个名为 phone 的变量,并同时给它赋值为一个字符串,此时类型推导就能推断变量 phone 应该是一个字符串类型。
这种在声明变量的同时对其进行赋值的做法是很常见的。可见,很多时候我们并不需要刻意去对变量的类型进行约束,ts 会自动帮我们推导出它的数据类型。
即便一开始没有赋初始值,类型推导没法帮我们完成类型约束,我们手动进行类型约束的成本也不会太高。通常我们在定义变量、编写函数时,都知道该变量或该函数的参数、返回值应该是啥类型。这些时候就应该给它们添加上类型约束。
类型推导发生了很多地方,只要能根据现有的条件推导出类型,ts 一般都能帮我们自动完成类型推导。比如上图中的 result 变量,推导出的结果将会是 number 类型。因为 sum 函数的返回值推导出的结果是 number 类型,又将 sum 函数的返回值赋值给 result。所以我们写的 const result = sum(1, 2)
其实就等效于 const result: number = sum(1, 2)
一键重命名、一键转到定义
F2
:一次性重命名F12
:快速跳转到函数的声明位置
:::warning 测试的时候发现了一些问题,简单记录一下:
如果使用的是 es6 规范,可以跨文件实现一键重命名、一键转到定义
如果使用的是 commonjs 规范,那么跨文件实现一键重命名、一键转到定义都不好使
并且在使用模块化语句的时候,一直报错,目前还不知道原因,等学到模块化相关的内容之后,再重点看看。
如果直接在 js 中使用 es6 module 来写,也可以实现上述所记录的「一键重命名」、「一键转到定义」的效果。 :::
识别某个变量是否添加了类型约束
在编辑器 vscode 中判断是否有类型约束的方式:在 vscode 编辑器中,看变量左下角是否有三个点,若有三个小点,表示没有进行类型约束,不知道是啥类型,它可以是任意类型 any。
上面这种写法,类型推导也推导不出具体是啥类型,最终结果只能是 any 类型。
any 表示任意类型,对该类型,ts 不进行类型检查。
编译结果对比
执行命令 tsc
,对比编译后生成的 js 文件和我们写的 ts 文件的差异:
会发现只有类型约束没了,其他的内容完全没变。
Q & A
🤔 电话号码、身份证,应该约束为 number 还是 string? 答:string 类型
识别方式:如果我们是按照个十百千这种方式来读,那么应该是 number,否则应该是 string
以该电话号码为例:15157722155
如果我们将其读作:么五么五七七二二么五五,那么应该将其视作 string 类型
如果我们将其读作:一百五十一亿五千七百七十二万两千一百五十五,那么应该是 number 类型
同理:学号、工号,等等。都应该是字符串类型。
3-2. 基本类型
前言
任务列表
-
strickNullChecks
- 掌握基本类型约束的语法
- 数字
:number
- 字符串
:string
- 布尔值
:boolean
- 对象
:object
- 数组
:number[]
、:Array<number>
- 空
:null
- 未定义
:undefine
- 数字
参考资料
compilerOptions.strictNullChecks
开启更加严格的空类型检查。
notes
基本类型
- 数字
:number
- 字符串
:string
- 布尔值
:boolean
- 对象
:object
- 数组
:number[]
、:Array<number>
- 空
:null
- 未定义
:undefine
null
和 undefined
是所有其他类型的子类型,它们可以赋值给其他类型。
通过添加 strictNullChecks:true
配置,可以获得更严格的空类型检查,null
和 undefined
只能赋值给自身。
codes
function isOdd(n: number): boolean { }
封装 boolean 函数,当我们写到这一步时,boolean 会有警告。因为我们还没有开始写函数体,没有指定函数的返回值。
若函数的返回值的约束类型不为 “void” 或 “any”,那么该函数必须要写 return 指定返回值。
function isOdd(n: number): boolean {
return n % 2 === 0;
}
当我们写到这一步时,就不会有报错了
function isOdd(n: number) {
return n % 2 === 0;
}
返回值也可以不进行约束,ts 会智能地进行类型推导,它能够识别出表达式 n % 2 === 0 的结果必然是一个 boolean 类型。
如上图所示,ts 会自动识别出函数 isOdd 的返回值是 boolean 类型,所以我们可以直接省略对其返回值的类型声明。
可见,在很多情况下,ts 都能够帮我们完成类型推导。类型推导是否成功,在前面介绍过,就是看变量名左下角是否有三个小点。类型推导的结果,我们也可以查看,方式也很简单,直接将鼠标悬停在变量名上边即可。
let nums_1: number[] = [1, 2, 3];
let nums_2: Array<number> = [1, 2, 3];
number[]
是 Array<number>
的语法糖,它们俩是等效的。
表示的含义是:要求数组的每一项都是数字类型。
:::warning
为了防止在写 react 时,和 jsx 的语法发生冲突,推荐使用第一种写法:number[]
:::
let nums = [1, 2, 3];
我们也可以直接这么写,ts 会帮我们完成类型推导。将鼠标悬停在 nums 上,会提示 let nums: number[]
,表示 nums 是一个数字数组,nums 的每一个成员都是数字类型。
所以说,我们平时在写 ts 时,虽然还是用原来写 js 的习惯来写,但是代码的质量就会比之前要高很多,它能帮我们规避一些不必要的问题。
let user: object = {
name: "dahuyou",
age: 22,
};
object 虽然也具有一定的限制作用,但是它的限制作用不强。它只能限制我们给 user 变量赋值时必须赋一个对象,但是无法限制对象里面每个成员具体是什么类型。所以我们一般很少会使用它。
function printObj(obj: object) {
const vals = Object.values(obj);
vals.forEach((v) => console.log(v));
}
printObj({
name: "dahuyou",
age: 22,
});
需求:封装一个 printObj 函数,该函数传入一个对象作为参数,要求答应出传入的对象身上的所有属性值。
let str: string = undefined;
null
和 undefined
是所有其他类型的子类型,它们可以赋值给其他类型。
上面这种写法,并不会报错,因为 undefined
会被识别为 string
的子类型,上面这种赋值的方式是被允许的。
但是这样的操作往往是我们不希望看到的,我们可以通过在配置中添加 strictNullChecks: true
,以获得更严格的空类型检查,使得 null
和 undefined
只能赋值给自身。
在开启 "strictNullChecks": true
的情况下,会报错:不能将类型“undefined”分配给类型“string”。
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"lib": [
"es2016"
],
"outDir": "./dist",
"strictNullChecks": true
},
"include": [
"./src"
],
}
strictNullChecks
默认值是 false
,表示 null
和 undefined
是可以被赋值给其它类型的,并不会报错;
如果将其设置为 true
,表示开启更加严格的空类型检查,此时 null
和 undefined
将无法再赋值给其它类型;
3-3. 其它类型
前言
任务列表
- 掌握以下类型
- 联合类型
- viod 类型
- never 类型
- 字面量类型
- 元祖类型(Tuple)
- any 类型
- 理解「类型保护」
- 掌握如何使用
typeof
关键字在判断语句块中触发类型保护
- 掌握如何使用
notes
其它类型
除了上节介绍的基本类型外,ts 还给我们提供了一些其它常用的类型:
- 联合类型:多种类型任选其一;
- viod 类型:通常用于约束函数的返回值,表示函数没有任何返回;
- never 类型:通常用于约束函数的返回值,表示该函数永远不可能结束;
- 字面量类型:使用一个值进行约束;
- 元祖类型(Tuple):一个固定长度的数组,并且数组中的每一项的类型确定;
- any 类型:any 类型可以绕过类型检查,因此,any 类型的数据可以赋值给任意类型;
类型保护
当对某个变量进行类型判断之后,在判断的语句块中便可以确定它的确切类型。
联合类型可以配合类型保护进行判断。
类型保护的触发方式有很多种,后边会有对应的课程专门介绍,通常我们会使用 typeof 来触发基本类型(number、string、boolean 等)的类型保护。
codes
联合类型
let foo: string | number[] = [1, 2, 3];
// foo 可以是 string 类型,也可以是 number[] 类型。
因为 foo 是一个联合类型,它的类型此时是没法确定的,所以当我们想要去调用变量 foo 身上的 api 时,ide 是没法给我们提供准确的智能提示的。
类型保护
function test(x: number[] | string) {
let result: string;
x; // => (parameter) x: string | number[]
// 类型保护
if (typeof x === "string") {
x; // => (parameter) x: string
result = x.toUpperCase();
} else {
x; // => (parameter) x: number[]
result = x.toString();
}
return result;
}
console.log(test("abc")); // => ABC
console.log(test([1, 2, 3])); // => 1,2,3
联合类型配合类型保护,可以明确变量 x 具体是什么类型。
当 ts 能够明确某个变量具体是什么类型时,我们在调用它身上的一些方法时,都会有精准的智能提示。
类型保护,是指通过某种手段,在判断语句块中确定一个变量的具体类型。
let test: string | number | undefined;
if (typeof test === "string") {
console.log("test is string");
} else if (typeof test === "number") {
console.log("test is number");
} else {
console.log("test is undefined");
}
补充一个小细节:如果我们在 if 语句块外边,输入 test.to
那么 ide 给我们提供的智能提示中,会展示哪些 api 呢?
答案是:string 和 number 都可以访问的一些 api。
void 类型
function printMenu() { // function printMenu(): void
console.log("1. login");
console.log("2. singn up");
}
/*
void 表示该函数没有返回值
*/
never 类型
function throwError(msg: string): never {
throw new Error(msg);
console.log("123"); // 永远不会执行
}
/*
若我们不指定 throwError 函数的返回值,那么它推断出来的结果是:
function throwError(msg: string): void
在这种情况下,我们就需要手动约束,指定其返回值为 never,表示该函数永远不可能结束。
*/
function alwaysDoSomething(): never {
while (true) {
// ...
}
}
/*
若不进行约束,那么识别出函数的返回值为 void
function alwaysDoSomething(): void
此时,也需要我们进行手动约束。
*/
上述两种情况都会导致函数永远不会结束
- 函数执行过程中发生了错误
- 函数中出现了死循环
字面量类型
let gender: "male" | "female";
gender = "male";
/*
字面量约束,是一种很强力的约束,我们只能从指定的字面量中选值。
*/
let arr: [];
这也是一个字面量约束,表示 arr 只能赋值为一个空数组。
// 对象的字面量约束(注意语法)
let user: {
name: string;
age: number;
};
user = {
name: "dahuyou",
age: 22,
};
/*
对象也可以有字面量约束。
上述代码约束 user 对象需要具有两个属性:name,age;
并且:name 必须是 string 类型,age 必须是 number 类型;
*/
:::warning 我们一般不会使用上面这种写法来对一个对象进行约束,在学习了后边的知识后,我们一般会采用“接口”、“类”、“类型别名”。。。来对对象类型的数据进行约束。 :::
元祖类型
let tuple: [number, string];
tuple = [1, "1"];
/*
元祖类型(Tuple):一个固定长度的数组,并且数组中的每一项的类型确定;
要求 tuple 必须是一个数组,该数组必须有两项,第一项必须是 number 类型、第二项必须是 string 类型。
*/
any 类型
let data: any = "123";
let num: number = data;
/*
any 类型:any 类型可以绕过类型检查,因此,any 类型的数据可以赋值给任意类型;
上述代码不会报错,但是,我们往往不希望有这样的行为,所以 any 类型,非必要的情况下,最好不要使用。
*/
3-4. 类型别名
前言
任务列表
- 掌握类型别名的语法
:::tips 类型别名在实际开发中,偶尔会使用到,但是使用的评率并不高,因为后边还有更好地方式来取代它。 :::
notes
类型别名
类型别名用于对已知的一些类型定义名称,类型别名并不会生成到编译结果中。
语法:type 类型名称 = 具体的类型信息
codes
未使用类型别名
// 用户对象的类型约束
let u: {
name: string;
age: number;
gender: "male" | "female";
};
// 获取用户对象
function getUsers(): {
name: string;
age: number;
gender: "male" | "female";
}[] {
return [];
}
获取用户对象的方法,返回的是一个数组,数组的每一项都是一个 u。
使用上面这种写法,当然也可以实现效果,但是,这么写很不好,存在的主要问题有:
- 可读性差;
- 不易维护;
若用户对象的类型约束(对象结构)发生了变化,那么需要改动的地方很多;
咋们写代码讲究:高内聚、低耦合。
:::warning
多个字段之间的分隔符:name: string;
✅ 后面可以啥都不加name: string
✅ 也可以加分号name: string,
✅ 也可以加逗号
:::
let u;
function getUsers() {
return [];
}
使用类型别名
type Gender = "male" | "female";
type User = {
name: string;
age: number;
gender: Gender;
};
let u: User;
function getUsers(g: Gender): User[] {
return [];
}
let u;
function getUsers(g) {
return [];
}
Q & A
🤔 一句话介绍「类型别名」 类型别名就是将类型信息给记录到一个变量中,使用该变量就等价于使用该类型。
3-5. 函数的相关约束
前言
任务列表
- 函数重载
- 可选参数
- 默认参数
notes
函数重载
在函数(定义)实现之前,对函数调用的多种情况进行声明。
不会生成到编辑结果中。
可选参数
可以在某些参数名后加上问号,表示该参数可以不用传递。
不会生成到编辑结果中。
:::warning 可选参数必须在参数列表的末尾。
翻译一下这句话:可选参数后边的所有参数,必须都得是可选的。 :::
默认参数
语法和含义和 es6 一致。
:::warning 默认参数必然是可选参数。 :::
codes
函数重载
function combine(a: number | string, b: number | string): number | string {
if (typeof a === "number" && typeof b === "number") {
return a * b;
} else if (typeof a === "string" && typeof b === "string") {
return a + b;
}
}
这种写法未罗列出所有可能的情况,会报错。
function combine(a: number | string, b: number | string): number | string {
if (typeof a === "number" && typeof b === "number") {
return a * b;
} else if (typeof a === "string" && typeof b === "string") {
return a + b;
}
throw new Error("a 和 b 必须是相同的类型");
}
对于未能罗列出来的所有可能,我们可以通过 throw 一个错误来表示。
function combine(a: number, b: number): number;
function combine(a: string, b: string): string;
function combine(a: number | string, b: number | string): number | string {
if (typeof a === "number" && typeof b === "number") {
return a * b;
} else if (typeof a === "string" && typeof b === "string") {
return a + b;
}
throw new Error("a 和 b 必须是相同的类型");
}
combine(1, 2); // => 2
combine("1", "2"); // => 12
// combine(1, '2');
当我们写了函数重载后,再去调用函数,就会有智能提示:
我们还可以给每个重载添加上注释,这样我们在调用函数的时候,我们写的相关注释内容,也会显示出来。
/**
* 得到 a 和 b 相乘的结果
* @param a
* @param b
*/
function combine(a: number, b: number): number;
/**
* 得到 a 和 b 拼接的结果
* @param a
* @param b
*/
function combine(a: string, b: string): string;
// ...
可以上下翻页,共两页,表示该函数的参数传递只能有两种情况。
可选参数
function sum(a: number, b: number, c?: number) {
if (c) return a + b + c;
return a + b;
}
sum(1, 2); // => 3
sum(1, 2, 3); // => 6
可选参数,表示该参数可以不传递。
在冒号前面加上一个问号,表示该参数是可选的 ?:
其中 c 就是可选参数,它可能是 undefined 或 number。
默认参数
function sum_3(a: number, b: number, c: number = 3) {
if (c) return a + b + c;
return a + b;
}
sum_3(1, 2); // => 6
默认参数,写法上和 es6 的默认参数写法相同。
默认参数表示当我们没有传递这个参数时,该参数的值是什么。
由此可知,默认参数必然是可选的。
类型推断的结果:function sum_3(a: number, b: number, c?: number): number
从类型推断的结果中我们发现,c 被识别为一个可选参数。
function test(a: number, b: number, c?: number) {}
function test(a: number, b?: number, c?: number) {}
function test(a?: number, b?: number, c?: number) {}
因为我们在调用函数的时候,参数都是挨个传递的,不可能说我们传递了第二个参数,而没有传递第一个参数。
由此我们不难得出以下结论:
- 可选参数可以有多个,但是一定要出现在末尾。
- 若一个参数是可选的,那么后续参数必然也是可选的。
Q & A
🤔 默认参数一定是可选参数? yes
🤔 如果某个函数有多个参数,那么可选参数是否可以写在前边? no
放在可选参数后边的参数,必然也是可选参数。
// ✅
function test(a: number, b: number, c?: number) {
// ...
}
// ✅
function test(a: number, b?: number, c?: number) {
// ...
}
// ✅
function test(a?: number, b?: number, c?: number) {
// ...
}
// ❎
function test(a?: number, b: number, c: number) {
// ...
}
3-6. 练习:创建并打印扑克牌
打印扑克,忽略大小王。
参考代码
type Color = "♥" | "♠" | "♦" | "♣"; // 扑克花色
type NormalCard = {
// 一张牌
color: Color;
mark: number;
};
type Deck = NormalCard[]; // 一副牌
function createDeck(): Deck {
const deck: Deck = [];
for (let i = 1; i <= 13; i++) {
deck.push(
{
mark: i,
color: "♠",
},
{
mark: i,
color: "♣",
},
{
mark: i,
color: "♥",
},
{
mark: i,
color: "♦",
}
);
}
return deck;
}
function printDeck(deck: Deck) {
let result = "";
deck.sort(() => Math.random() - 0.5); // 乱序
deck.forEach((pocker, i) => {
const num = pocker.mark;
if (num <= 10) {
result += num + pocker.color + " ";
} else if (num === 11) {
result += "J" + pocker.color + " ";
} else if (num === 12) {
result += "Q" + pocker.color + " ";
} else if (num === 13) {
result += "K" + pocker.color + " ";
}
// 每打印 17 张牌,换行。
if ((i + 1) % 17 === 0) {
result += "\n";
}
});
console.log(result);
}
const deck = createDeck();
printDeck(deck);
🙈 老师代码,自己实现一遍
开发前的准备工作:
使用tsc --init
命令来初始化一个 ts 的配置文件,并编写好基本配置信息。
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"lib": [
"es2016"
],
"outDir": "./dist",
"strictNullChecks": true
},
"include": [
"./src"
],
}
编辑 package.json 文件,简化执行流程。
{
"scripts": {
"dev": "nodemon --watch src -e ts --exec ts-node ./src/pocker.practice.ts"
},
"devDependencies": {
"@types/node": "^16.11.26"
}
}
type Colors = "❤" | "♠" | "♣" | "◇";
type Poker = {
// 一张牌
number: number | string,
color?: Colors, // 大小王没有花色
};
type Deck = Poker[]; // 一副牌
// 创建一副牌
function createDeck() {
const deck: Poker[] = [];
// 插入 A ~ K
for (let i = 1; i <= 13; i++) {
let number: number | string = i;
if (i === 1) number = "A";
if (i === 11) number = "J";
if (i === 12) number = "Q";
if (i === 13) number = "K";
deck.push({
number,
color: "❤",
});
deck.push({
number,
color: "♠",
});
deck.push({
number,
color: "♣",
});
deck.push({
number,
color: "◇",
});
}
// 插入大小王
deck.push({
number: "joker",
});
// 插入大王
deck.push({
number: "JOKER",
});
deck.sort(() => Math.random() - 0.5); // 打乱牌序
return deck;
}
// 打印一副扑克
function printDeck(deck: Deck) {
let result = "底牌:";
const desk = deck.splice(0, 3); // 底牌
desk.forEach((poker, i) => {
result += poker.color ? ` ${poker.color}${poker.number}` : ` ${poker.number}`;
});
result += '\n用户1:'
deck.forEach((poker, i) => {
if(i % 17 === 0 && i !== 0) result += `\n用户${i / 17 + 1}:`;
result += poker.color ? ` ${poker.color}${poker.number}` : ` ${poker.number}`;
});
console.log(result);
}
const deck = createDeck();
printDeck(deck);
export {};
写了一个类似于斗地主发牌的 demo。