Typescript 教程
1.Typescript 简介
1.1 介绍
● Typescript 是 JavaScript 的超集,本质上是为了扩展 JavaScript 的功能,让其更加适合企业级的开发。它的目的并不是创造一种全新语言,而是增强 JavaScript 的功能。
● 由微软发布于 2012 年,作者是 C# 的首席架构师。
● 最终 Typescript 都会被编译为 JavaScript 执行。
1.2 优势
● 强大的类型提示
● 适合大型项目的开发
1.3 类型的概念
类型(type)指的是一组具有相同特征的值。如果两个值具有某种共同的特征,就可以说,它们属于同一种类型。
类型的目的就是为了编程约束和语法提示。
function add(a: number, b: number) {return a + b
}
add(1, '1') // 报错
比如如上的代码是会报错的。
JavaScript 本身就不具备这样的功能,所以一般被认为是动态语言。
GPT: JavaScript 是一种动态语言,这意味着在运行时执行类型检查。JavaScript 变量的类型是在运行时确定的,因此您可以在运行时更改变量的类型。这使得 JavaScript 灵活,但也容易出现类型相关的错误。
TypeScript 是 JavaScript 的一个超集,它被称为静态语言,因为它在编译时执行类型检查。在 TypeScript 中,您必须在声明变量时指定其类型,并且类型检查是在编译时执行的,而不是在运行时。这可以帮助捕获潜在的类型错误,并提供更好的代码可维护性和可读性。
1.4 静态类型的优缺点
以下是一些静态语言的优点:
-
类型安全:静态类型语言在编译时进行类型检查,因此它们能够在代码运行之前捕获许多潜在的类型错误。这有助于减少在运行时出现的类型相关错误,提高代码的稳定性和可靠性。
-
更好的可读性和维护性:类型声明和注释可以提供更好的文档,使代码更易于理解。开发者可以清晰地看到变量的类型,从而更容易理解代码的意图。这对于大型项目和团队协作特别有帮助。
-
智能编码工具:静态类型语言通常与集成开发环境(IDE)和其他开发工具配合得很好。IDE可以提供智能代码完成、类型检查和重构功能,以帮助开发者更快地编写和维护代码。
-
提前发现错误:由于类型检查发生在编译时,开发者可以在代码运行之前发现许多潜在的问题和错误。这有助于减少调试时间和提高代码质量。
-
优化性能:静态类型信息可以帮助编译器和运行时系统优化代码,从而提高执行效率。这意味着静态语言的程序通常可以更快地运行。
-
丰富的工具生态系统:许多静态类型语言拥有丰富的库和框架,这些工具可以加速开发过程,并提供各种功能,从而减少了自己编写大部分代码的需求。
-
更严格的代码约束:静态类型语言通常强制执行更严格的代码约束,这可以帮助开发者遵循良好的编程实践,减少代码中的混乱和错误。
总的来说,静态类型语言在大型项目、团队协作以及对代码质量和可维护性有高要求的情况下往往更受欢迎。然而,每种语言都有其自身的优点和局限性,开发者需要根据项目需求和团队的技能来选择适当的编程语言。
缺点:
-
冗长的类型声明:在静态类型语言中,您通常需要显式声明变量的类型,这可能会导致冗长的类型注释和声明,特别是在复杂的数据结构或泛型代码中。
-
学习曲线:静态类型语言通常需要开发者花更多的时间来学习类型系统和语言规范。这可能导致初始开发速度较慢,特别是对于新手来说。
-
灵活性不足:类型检查可能会限制一些动态编程语言中的灵活性。有时候,您可能需要做一些类型不安全的操作,而静态类型语言可能会限制这种灵活性。
-
编译时间延迟:静态类型语言需要进行编译,这可能会导致开发周期中的一些延迟,特别是在大型项目中,编译时间可能会变得较长。
-
陡峭的学习曲线:尤其是对于初学者来说,学习静态类型语言可能会比学习动态类型语言更具挑战性,因为它们通常要求更多的类型知识和规范。
-
冗余的类型信息:在某些情况下,静态类型语言可能需要开发者提供冗余的类型信息,这可能会使代码变得冗长和难以维护。
-
类型注解维护:如果代码的结构发生变化,类型注解也需要相应地更新。这可能会增加代码维护的工作量。
-
适应性:某些项目或领域,如原型开发、小型脚本和快速迭代的项目,可能更适合使用动态类型语言,因为它们能够提供更大的灵活性和快速开发速度。
所以,请理性看待 Typescript 的使用。
2.Typescript 基本用法
2.1 类型声明
使用 : 类型 的形式来标注类型即可。
function add(a: number, b: number): number {return a + b
}
add(1, 1)
2.2 类型推断
但是对于 Typescript ,类型推断并不是必须的,因为 Typescript 会自己进行类型推断。
比如:
Typescript 会默认你的 a 的类型是 number。
从这里可以看到,TypeScript 的设计思想是,类型声明是可选的,你可以加,也可以不加。即使不加类型声明,依然是有效的 TypeScript 代码,只是这时不能保证 TypeScript 会正确推断出类型。由于这个原因,所有 JavaScript 代码都是合法的 TypeScript 代码。
所以在将 JS 的项目改编为 TS 的时候,可以只更改少量的代码即可完成从 JS 到 TS 的转变。
2.3 编译
因为 TS 本身没有运行环境,所以需要将 TS 编译为 JS 然后执行,JS 的运行环境就是浏览器和 Node。
TypeScript 的类型检查只是编译时的类型检查,而不是运行时的类型检查。一旦代码编译为 JavaScript,运行时就不再检查类型了。
2.4 值与类型
TS 编写的程序可以分为值代码和类型代码,TS 的本身只处理类型代码,而值代码依然还是由 JS 处理的。它们是可以分离的,TypeScript 的编译过程,实际上就是把“类型代码”全部拿掉,只保留“值代码”。
2.5 编译器
官方给我们提供了 tsc 编译器,来将 .ts 文件编译为 .js 文件。
1.全局安装:npm install -g typescript 2.检查是否安装成功:tsc -v,会打印出版本 3.编译 TS 文件:tsc file1.ts file2.ts file3.ts 即可
参数:
–outFile:指定编译之后的文件,如果是编译多个 TS 文件,则是将多个编译结果放在这个文件中。
tsc file1.ts file2.ts --outFile app.js
–outDir:指定编译后的 JS 文件存放的位置。
tsc app.ts --outDir dist
–target:指定编译后的 JS 代码的 ES 版本,默认是 ES 3.0,我们也可以自行指定。
tsc --target es2015 app.ts
编译错误的处理:
尽管有错,tsc 依然原样将 TypeScript 编译成 JavaScript 脚本。这是因为 TypeScript 团队认为,编译器的作用只是给出编译错误,至于怎么处理这些错误,那就是开发者自己的判断了。
这时候又会使用另外的两个参数 --noEmit 和 --noEmitOnError,前者只会检查是否有类型错误,后者是只在没有类型错误的时候才会生成编译后的文件,否者就不会想原来一样报错了也会生成文件。
2.6 tsconfig.json
tsconfig.json 是 TS 的编译参数,我们可以将上面讲到的几个参数都写在这个文件中,编译的时候就会按照这些参数编译。
比如编译结果的参数:
{"files": ["file1.ts", "file2.ts"],"compilerOptions": {"outFile": "dist/app.js"}
}
这时候我们只需要执行 tsc 即可,TS 会自行根据配置文件编译。
2.7 ts-node
一个非官方的模块,可以将编译和执行步骤打包到一起执行。
1.安装:npm install -g ts-node 2.执行:ts-node file.ts
如果只是想简单的看一下结果,那么可以使用 ts-node。
3.三种特殊类型
即 any、unknown、never。
3.1 any 类型
3.1.1 基本含义
any 类型表示没有任何限制,该类型的变量可以赋予任意类型的值。
实际上只要被赋予了 any 类型,TS 就会关闭对这个变量的检查,这显然违背了使用 Typescript 的初衷。
我们一般情况下不会使用 any 类型,除非你故意的。
会使用 any 的两种情况:
● 特殊原因,真的需要关闭对某一个变量的检查;
● 为了适配老项目,无法将变量的类型准确定义;
总之,TypeScript 认为,只要开发者使用了any类型,就表示开发者想要自己来处理这些代码,所以就不对any类型进行任何限制,怎么使用都可以。
从集合论的角度看,any类型可以看成是所有其他类型的全集,包含了一切可能的类型。TypeScript 将这种类型称为“顶层类型”(top type),意为涵盖了所有下层。
3.1.2 类型推断问题
在函数中,如果你没有指定类型,那么按照之前的说法,TS 会自行推断,如果 TS 推断不出来,那么就会被赋值为 any。
function add(x, y) {return x + y;
}
add(1, [1, 2, 3]) // 不报错
所以,在写函数的时候,我们最好将变量的类型写上,来帮助 TS 做推断,否则很容易产生 any 类型。
此时我们可以开启一个参数,来强制开发者书写相关代码:
tsc --noImplicitAny app.ts
如果推断出了 any 类型,就会直接报错,但是注意,不包含 let 和 var 定义的变量。
注意:这个参数可以使用 any,只是在推断出 any 类型时报错。
3.1.3 污染问题
因为 any 类型相当于关闭了类型检查,所以 any 类型的值可以赋值给其他所有类型的值。但是这个时候就会造成类型的污染,导致 TS 编译可以通过,但是实际运行肯定是有问题的。
let x: any = 'hello'
let y: numbery = x // 编译不报错console.log(y * 123) // 编译不报错
y.toFixed() // 编译不报错
比如以上的代码,编译的时候肯定不会报错,但是很明显 y 此时为 ‘hello’,执行 y * 123 尚且可以被 JS 运行出 NaN,但是 toFixed 就百分百报错了。
注意这里一定要谨记阮一峰老师的教诲,值是值,类型是类型,此时的 y 的类型是 number,但是它的值是 ‘hello’。
3.2 unknown 类型
unknown 类型用于表示变量的类型是未知的,强制开发者在使用之前进行类型检查,以提高类型安全性。
unknown 某种程度上和 any 类型,都可以将任意类型的值赋给这个类型,但是你无法将这个类型赋值给除了 any 和 unknown 之外的类型,并且无法再确定类型之前使用其任何的方法。如果想使用这个变量,必须先进行类型缩小。
必须这样写:
这样就避免了 any 的类型污染问题。
3.3 never 类型
为了保持与集合论的对应关系,以及类型运算的完整性,TypeScript 还引入了“空类型”的概念,即该类型为空,不包含任何值。简单来说就是不存在的情况。
使用场景:
1.else 分支的最后一个不存在的情况
function fn(x:string|number) {if (typeof x === 'string') {// ...} else if (typeof x === 'number') {// ...} else {x; // never 类型}
}
2.不可能有返回值
function f():never {throw new Error('Error');
}
此外,never 还有一个特性,就是可以赋值给任意类型,因为集合论规定了空集是任何集合的子集,所以它属于一个底层类型。而 any 和 unknown 属于顶层类型。
4.Typescript 的类型系统
4.1 基本类型
和 JavaScript 中的类型一样,包含以下的八种:
● string
● number
● boolean
● null
● undefined
● bigInt
● symbol
● object
这八种类型也是 TS 类型的基础,复杂类型由他们组合而成。
注意几个点:
- undefined 和 null 既是类型也是值,只包含 undefined 和 null
- object 包含了数组、对象 、函数等引用类型
- bigInt 和 number 是两种类型
- 如果给一个变量赋值为 undefined 而没有指定类型,这个变量会被确定为 any 类型,null 同样如此,解决方法就是开启严格模式:strictNullChecks
4.2 包装对象类型
4.2.1 包装对象的概念
我们可以将常见的八种类型分为三类: - 值类型:undefined 和 null - 复合类型:object - 基本类型:string、number、boolean、bigInt、symbol
在 JS 中,有一个包装对象的概念,比如我们调用 ‘123’.charAt(0) 时:
在字符串字面量 ‘123’ 上调用 charAt 方法时,JavaScript 首先会尝试查找 ‘123’ 这个字符串字面量对象上是否有 charAt 方法。由于字符串字面量是不可变的,所以 JavaScript 会创建一个临时的字符串包装对象,该对象具有与字符串字面量相同的值,并且继承了字符串对象的原型链,因此可以调用 charAt 方法。这个临时的字符串包装对象是 String 类型的实例,它继承了 String 对象的原型方法。
注意:bigInt、symbol 无法直接获取包装对象。
可以通过以下的方式获取,但是获取了也没有什么使用的场景。
let a = Object(Symbol());
let b = Object(BigInt());
4.2.2 包装对象类型与字面量类型
TS 针对包装对象和字面量对象进行了特殊的类型优化,即大小写双版本。大写开头的即代表包装对象,同时也可以代表字面量,但是小写开头的只能代表字面量。
建议使用小写版本,因为我们一般也都是直接使用字面量,此外 TS 内部的类型也大量使用小写,如果使用大写标注,会导致一些方法编译失败。
比如:
const n1:number = 1;
const n2:Number = 1;Math.abs(n1) // 1
Math.abs(n2) // 报错
4.3 Object 类型和 object 类型
4.3.1 Object 类型
在 JS 中所有可以转为对象的值都可以被标记为 Object 类型。也就是除了 undefined 和 null 以外,其他的所有的值否可以被标记。
let obj:Object; // 也可以写为 let obj:{}obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
4.3.2 object 类型
与大写的相对应,小写的 object 就代表着狭义的对象,即对象、数组和函数。
但是注意不管是大写还是小写,访问对象的时候都正能够访问对象原生的方法,访问自定义的属性和方法都是会报错的。
4.4 undefined 和 null 的特殊性
1.任何类型都可以赋值和 undefined 和 null,目的其实是为了和 JS 保持一致,比如一个变量没有赋值,我们在 JS 中就可以给它先赋值为 undefined。 2.开启 “strictNullChecks”: true后就无法将 undefined 和 null 赋值给其他类型,当然 any 和 unknown 还是可以的。
4.5 值类型
就是一个值就是代表着一个类型。
const a = 5 // 这时候 a 的类型是一个数字 5,并不是想象中的 number// 如果指定的话,就可以确定基本类型
const b: number = 5
但是不管怎样其实都是合理的,因为 const 规定改过之后就无法再次修改了。
这有一些特殊情况:
因为右侧被识别为了 number 类型,左侧类型是 5,所以无法将大集合赋值给小集合。
4.6 联合类型
就是多种类型的集合。
let setting:true|false;let gender:'male'|'female';let rainbowColor:'赤'|'橙'|'黄'|'绿'|'青'|'蓝'|'紫';
比如我们之前的 null 类型,如果开启了 strictNullChecks 之后就无法将 null 赋值给 null 、any、unknown 以外的类型,那么如果这个值的初始值没有,需要赋值为 null,我们就可以使用联合类型来操作。
let name:string|null;name = 'John';
name = null;
此外,如果使用了联合类型 ,在使用值的 api 的时候需要确定值的具体类型,即进行类型缩小,否则会报错。
function printId(id:number|string
) {console.log(id.toUpperCase()); // 报错
}// 应该如下写
function printId(id:number|string
) {if (typeof id === 'string') {console.log(id.toUpperCase());} else {console.log(id);}
}
4.7 交叉类型
和联合类型一样,也是多个类型组成一个新的类型,但是这个是且的关系。
let x:number&string; // 就是 x 的值需要既是数字又是字符串,显然不可能,所以会被直接识别为 neverlet obj:{ foo: string } &{ bar: string };obj = {foo: 'hello',bar: 'world'
};
4.8 type 命令
type 命令用来定义一个类型的别名,别名不允许重复。
此外别名是作用在块级作用域,不同作用域名字肯定是可以一样的。
4.9 typeof 运算符
注意在 TS 中的 typeof 运算符与 JS 中的不同,JS 中是直接返回类型字符串,而 TS 中是返回的是该值的 TS 类型,比如:
let obj = {a: 0}
type T = typeof obj // 返回值为 {a:number}
但是注意,并不是说你在写 TS 代码的时候所有的 typeof 都是类型运算了,实际上写 TS 的时候是存在两种 typeof 运算符的。
let a = 1;
let b:typeof a;if (typeof a === 'number') {b = a;
}
以上的代码中就可以看出两种 typeof 的使用。
4.10 块级类型声明
块级类型声明就是类型声明可以写在代码块中,并且也在当前代码块中有效。
let flag: boolean = true
if (flag) {type T = numberlet b: T = 123
} else {type T = stringlet b: T = '123'
}
4.11 类型的兼容
就是不同类型之间的赋值问题。
let a: number | string = 1
let b: string = '1'
a = b // number | string = string ,此时 a 的类型变为了 string,发生了类型收窄
b = alet a1: 'hi' = 'hi'
let b1: string = 'hello'b1 = a1 // 正确,且不会发生类型收窄
a1 = b1 // 报错 为什么报错呢?因为ts认为a1只能是hi,所以不能赋值给b1type T = typeof alet c: T = 1 // 报错:number 无法赋值给 string
5.数组
5.1 简介
TS 的数组类型由两种表示方式,一种是直接使用类型加[],一种是使用泛型。
联合类型:let arr:number[] = [1, 2, 3]; 泛型:let arr:Array = [1, 2, 3]
5.2 数组的类型判断
在一开始数组为空的情况下,TS 会在改变数组的时候动态更改数组的类型。
const arr = [];
arr // 推断为 any[]arr.push(123);
arr // 推断类型为 number[]arr.push('abc');
arr // 推断类型为 (string|number)[]
但是如果一开始数组中有值,就会认为所有值都是这个类型,添加其他类型的值则会报错。
// 推断类型为 number[]
const arr = [123];arr.push('abc'); // 报错
5.3 只读数组,const断言
声明只读数组有以下几种方式:
1.as const
const arr = [0, 1] as const;arr[0] = [2]; // 报错
2.readonly number[]
const arr:readonly number[] = [0, 1];arr[1] = 2; // 报错
arr.push(3); // 报错
delete arr[0]; // 报错
此外,readonly 不能与泛型的形式定义只读数组,如果要使用泛型的形式,可以使用如下的两种专门提供的两种方法:
const a1:ReadonlyArray<number> = [0, 1];const a2:Readonly<number[]> = [0, 1];
注意:只读数组是普通数组的父类!!!
5.4 多维数组
TypeScript 使用T[][]的形式,表示二维数组,T是最底层数组成员的类型。
var multi:number[][] = [[1,2,3], [4,5,6]];
上面示例中,变量multi的类型是number[][],表示它是一个二维数组,最底层的数组成员类型是number。