TypeScript
js 与 ts 的关系
1.js本身没有对变量、函数参数进行类型限制;
2.这样虽然灵活但是也有安全隐患;
3.之后facebook 推出了 flow,微软推出了 ts,他们都是致力于为 js 提供类型检查,而不是取代;
4.并且在ts 的官方文档上有这么一句话:源于 js,归于 js!;
5.最终ts还是需要转换成 js 代码才能运行的;
6.当然不排除有一天 js 本身也会加入类型检测,那么无论是 ts 还是 flow 都有可能因此退出历史舞台;
型检测
共识
错误出现的越早越好
能在写代码时发现错误,就不要再代码编译时再发现(IDE的优势就是在代码编写过程中帮助我们发现错误);
能在代码编译时发现错误,就不要再代码运行时再发现(类型检测就可以很好的帮助我们做到这一点);
能在开发阶段发现错误,就不要在测试期间发现错误;
能在测试期间发现错误,就不要在上线后发现错误;
来看这么一段代码
function foo(message) {
console.log(message.length);
}
foo("hello");
// 后面有其它代码
运行,结果是 5
这么看来这段代码基本是没有什么问题的;
但是存在一个非常大的隐患:当执行**foo()**不传任何参数时,就报错了;
由于没传参数,导致 foo()中的message 是 undefined,undefined 哪来的 length?
这个报错导致后面的代码无法执行,这就导致一行报错,后面所有代码都无法运行,这是你希望看到的吗?;
上面函数并没有对参数进行校验:
- 参数类型
- 是否传参
这时,TypeScript 应运而生。
TypeScript
ts 是拥有类型的js 超集,它可以编译成普通、干净、完整的 js 代码。
js 所有的特性,ts 都支持,并且它紧随 ECMAScript 的标准,所以 es6、es7、es8 等新语法标准,ts 都是支持的;
在语言层面上,不仅仅增加了类型约束,而且包括一些语法的拓展,比如枚举类型(Enum)、元组类型(Tuple)等;
所以,可以将 ts 理解为加强版的 js;
从开发者长远的角度看来,学习 ts 有助于培养类型思维,这种思维对于完成大型项目尤为重要。
TypeScript 官网:https://www.typescriptlang.org/
编译环境
我们知道 ts 是需要转换成 js 的,那谁来负责转换呢?
- tsc,TypeScript Compiler
- babel
全局安装
npm install typescript -g
tsc --version
查看版本
体验
来编写一段 ts 代码
function foo(message: string) {
console.log(message.length);
}
foo("hello");
当你执行 foo()不传参数或者传入非 string 类型的参数,编辑器就会提醒你了,不需要等到运行期间才报错;
然后执行 tsc ts文件
命令,转化为 js 文件,这时会出现同名的 js 文件;
转化后的代码
function foo(message) {
console.log(message.length);
}
foo("hello");
搭建 ts 编译环境
当然,真实开发不是写一个 ts 文件转换一个
我们需要:
- 编写完 ts,它自动转换成 js;
- 然后自动在浏览器上运行;
- 也可以自动更新内容;
搭建方式有几种:
- 通过webpack搭建;
- 安装 node 的一个库ts-node;
使用 ts-node 库搭建
ts-node 做了什么事情?
将 ts 转换成 js,然后在 node 环境上运行
先全局安装 npm install ts-node -g
;
而 ts-node 又依赖于两个库,tslib、@types/node,全局安装它们 npm install tslib @types/node -g
测试
编写这么一段 ts 代码
const name: string = "abc";
const age: number = 18;
console.log(name);
console.log(age);
export {};
为什么要写 export {}
?
因为 ts 文件默认使用全局作用域(所有的 ts 文件),使用 export 语法变成模块,就有独立的作用域了,防止命名冲突
执行 ts-node ts文件
命令,你会看到编辑器的终端打印 abc 和 18
使用 webpack 搭建
本地安装ts-loader和typescript,因为 ts-loader 本身又依赖于 typescript
npm install ts-loader typescript -d
配置 webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.ts$/,
loader: ts - loader,
},
],
},
};
同时还需要tscconfig.js这个属于 ts 的配置文件,可以使用 tsc --init
生成;
最后还需要搭建一个本地服务,利用 webpack-dev-server;
本地安装
npm install webpack-dev-server -d
变量声明
声明的类型可以称之为类型注解;
let/const 标识符: 类型 = 值
例子
let msg: string = "hhh";
小写和大写区别
类型的小写:TypeScript 中的类型;
类型的大写:JavaScript 中的类型对应的包装类;
类型推断
默认情况下进行赋值时,会将赋值的类型,作为标识符的类型
例子
let msg = "hhh";
msg 默认的类型就是字符串类型
数据类型
- number 类型
- boolean 类型
- 字符串类型
- Array 类型
- object 类型
- null 和 undefined
- Symbol 类型
- any 类型
- unknown 类型
- void 类型
- never 类型
- tuple 类型
Array 类型
规定一个数组类型并且元素是 string 类型的例子
写法一,不推荐,可能会和 jsx 冲突
const names: Array<string> = [];
写法二,推荐
const names: string[] = []
any 类型
类型不限制
tuple 类型
区别于数组类型,数组的元素类型一般一致,只有**“共性”**;
而元组可以保留元素的**“个性”**;
元组通常可以作为返回的值,在使用时会非常方便;
const zsf: [string, number, number] = ["hhh", 18, 100];
联合类型
扩大类型的范围;
ts 的类型系统允许使用多种运算符,从现有的类型中构建新类型;
联合类型中每个类型被称之为联合成员(union’s members);
function printID(id: number | string) {
console.log(id);
}
联合类型和可选类型有点类似;
function printID(id?: number) {
console.log(id);
}
可以转换成
function printID(id: number | undefined) {
console.log(id);
}
结合字面量类型
其实字符串也是当成类型,也就是字面量类型;
而字面量类型的值只能和类型一致;
const msg: "hhh" = "hhh";
这时字面量类型就可以和联合类型结合使用了,不和结合类型结合使用,字面量类型没什么意义
let align: "left" | "right" | "center";
align = "right";
类型别名
当联合类型有很多联合成员时,类型会非常长,可读性可能不好;
此时可以给类型起别名,使用关键字type定义;
type IdType = number | string;
type PointType = {
x: number;
y: number;
z?: number;
};
function printID(id: IdType) {
console.log(id);
}
function printPoint(point: PointType) {
console.log(point.x, point.y, point.z);
}
类型断言 as
有时候 ts 无法获取具体的类型信息,这时就需要使用类型断言(Type Assertions);
比如 document.getElementById(),ts 只知道该函数会返回HTMLElement,但并不知道它具体的类型;
通过类型断言可以把一个范围较大的类型转化为更为具体的类型;
比如这一段代码
const el = document.getElementById("hhh");
el.src = "...";
当直接使用src属性时,编辑器会提示错误,因为 HTMLElement 类型有很多,有的并没有 src 属性;
而将类型断言为HTMLImageElement,一定有 src 属性;
const el = document.getElementById("hhh") as HTMLImageElement;
el.src = "...";
非空类型断言
看这一段代码
function foo(msg?: string) {
console.log(msg.length);
}
这段编译阶段是不通过的,参数是可选类型,当没传参数时 msg 就是 undefined,undefined 哪来的 length?
一般可以加个if 判断;
function foo(msg?: string) {
if (msg) {
console.log(msg.length);
}
}
也可以使用非空类型判断
function foo(msg?: string) {
console.log(msg!.length);
}
感叹号!就可以确保 msg 一定有值;
可选链
可选链并不是 ts 独有的特性,在 es11 中 js 也增加了这一特性;
操作符是?
;
它的作用是当对象的属性不存在时,会短路,直接返回undefined,如果存在,才会继续执行;
type Person = {
name: string;
friend?: {
name: string;
age?: number;
};
};
const info: Person = {
name: "zsf",
};
console.log(info.friend?.name);
??和!!
!!操作符将其它类型转化为boolean 类型;
const msg: string = "hh";
const flag = !!msg;
console.log(flag);
空值合并运算符??,es11 新增,类似于三元运算符
let msg: stirng | null = null;
const content = msg ?? "hhh";
console.log(content);
等价于
let msg: stirng | null = null;
const content = msg ? msg : "hhh";
console.log(content);
类型缩小
可以通过类似 typeof padding === 'number'的判断语句来改变 ts 的执行路径;
在给定的执行路径中,可以缩小比声明时更小的类型,这个过程叫类型缩小,也叫类型保护;
常见的类型保护有:
- typeof
- 平等缩小(===、!==)
- instanceof
- in
- 等等
type IDType = number | string;
function printID(id: IDType) {
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id);
}
}
本来是number 和 string 的联合类型,typeof id === 'string'
这个判断将类型缩小为 string 类型;
这样才能使用toUpperCase(),number 类型是没有这个方法的;
函数相关的类型
参数
function sum(num1: number, num2: number) {
return num1 + num2;
}
匿名函数的参数类型
const names = ["hhh", "abc", "nb"];
names.forEach((item) => {
console.log(item.split(""));
});
item 的类型可以根据上下文推断出来,这时可以不加类型注解;
复杂的参数一般用对象类型
比如打印一个点坐标的函数,参数是坐标,较为复杂,可以使用对象类型限制
function printPoint(point: { x: number; y: number }) {
console.log(point.x, point.y);
}
返回值
可以不写返回值类型,会自动推断;
function sum(num1: number, num2: number): number {
return num1 + num2;
}
如果没有返回值
function sum(num1: number, num2: number): void {
return num1 + num2;
}
可选类型
有些点没有 z 坐标,所以 z 参数可选;
可选类型放必选类型后面;
function printPoint(point: { x: number; y: number; z?: number }) {
console.log(point.x, point.y, point.z);
}
函数的类型
在 js 中,函数是重要的组成部分,并且函数可以作为一 等公民(可以作为参数,也可以作为返回值);
既然 ts 中传递参数要类型注解,那把函数当参数传递时应该怎么写类型注解呢?
function foo() {}
type FooType = () => void;
function bar(fn: FooType) {
fn();
}
bar(foo);
声明函数时,另一种编写类型的方式
type AddFnType = (num1: number, num2: number) => number;
const add: AddFnType = (a1: number, a2: number) => {
return a1 + a2;
};
可推导的 this 类型
this在不同情况下会绑定不同的值,所以对于它的类型就更难把握了;
默认推导
const info = {
name: "zsf",
eating() {
console.log(this.name + "eating");
},
};
info.eating();
ts 默认推导 this 就是 info 对象;
如果 ts 能推导出 this,那就可以放心使用 this;
但是下面这种情况 ts 推导不出 this
function eating() {
console.log(this.name + "eating");
}
const info = {
name: "zsf",
eating: eating,
};
info.eating();
这段代码如果放在 js 中,this 绑定的是 info 对象;
但在 ts 中,它推导不出 this,要是想能让 ts 推导出,第一个参数得传 this;
type ThisType = {
name: string;
};
function eating(this: ThisType) {
console.log(this.name + "eating");
}
const info = {
name: "zsf",
eating: eating,
};
info.eating();
函数的重载
先来实现一个简单的函数:对两个数字或者字符串执行+的操作
以前的做法: 使用联合类型,以及一些逻辑判断(类型缩小)
type AddType = number | string;
function add(a1: AddType, a2: AddType) {
if (typeof a1 === "number" && typeof a2 === "number") {
return a1 + a2;
} else if (typeof a1 === "string" && typeof a2 === "string") {
return a1 + a2;
}
}
add(10, 20);
使用联合类型实现有两个缺点:
- 进行很多的逻辑判断(类型缩小);
- 返回值的类型依然是不能确定的;
这时使用函数重载就可以啦
ts 中的函数重载是函数名一样,参数不一样(类型、数量),不用具体实现;
而函数的具体实现,参数用的是广泛的any 类型;
function add(num1: number, num2: number): number;
function add(num1: string, num2: string): string;
function add(num1: any, num2: any) {
return num1 + num2;
}
add(10, 20);
有时候,联合类型和函数重载都可以实现某个逻辑,优先使用联合类型,如果联合类型实现起来复杂,那才考虑重载
ts 中类的使用
基本使用
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
eating() {
console.log(this.name + "eat");
}
}
const p = new Person("zsf", 18);
继承,多态,重写等等语法类似~
成员修饰符
在 TypeScript 中,类的成员支持三种修饰符:public、private、protected;
public修饰任何地方可见、公有的成员,默认;
protected修饰的是仅在类自身及子类中可见、受保护的成员;
private修饰的是仅在同一类中可见、私有的成员;
只读属性
readonly,是属性的修饰符;
只读属性只能在构造器中赋值,且赋值后不可改变;
属性本身不能修改,但属性要是对象类型,那对象的内容可以修改(和const声明的对象类似);
class Person {
readonly name: string;
constructor(name: string) {
this.name = name;
}
}
const p = new Person("zsf");
console.log(p.name);
访问器
和 setter/getter 写法有点区别;
有个规范,私有属性一般在前面加个下划线_
;
class Person {
private _name: string;
constructor(name: string) {
this._name = name;
}
set name(newName) {
this._name = newName;
}
get name() {
return this._name;
}
}
const p = new Person("zsf");
console.log(p.name);
p.name = "hhh";
静态成员
static 修饰
不用实例化,可以通过类直接访问
抽象类
定义通用接口时,通常会让调用者传入父类,通过多态来实现更加灵活的调用方式;
但是,父类本身可能并不需要对某些方法进行具体实现,这时就可以定义为抽象方法;
function makeArea(shape: Shape) {
return shape.getArea();
}
abstract class Shape {
abstract getArea();
}
class Rectangle extends Shape {
private width: number;
private height: number;
constructor(width: number, height: number) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Circle extends Shape {
private r: number;
constructor(r: number) {
super();
this.r = r;
}
getArea() {
return this.r * this.r * 3.14;
}
}
const rectangle = new Rectangle(20, 30);
const circle = new Circle(10);
console.log(makeArea(rectangle));
console.log(makeArea(circle));
防止 makeArea()传入其它类型导致无法计算面积,应该限制只能传Shape类型;
所以Circle和Rectangle应该继承 Shape;
抽象类不能被实例化,防止makeArea(new Shape())
,应该将 Shape 类变成抽象类;
抽象类的抽象方法没有具体实现(没有方法体),具体实现交给子类;
抽象类的子类,必须实现其父类的抽象方法;
所以你看,这样的代码不是严谨、安全许多了吗?
类的类型
和使用 type 给类型起别名有点类似~
class Person {
name: string;
}
const p: Person = {
name: "hhh",
};
ts 中接口的使用
我们知道,通过 type 可以声明对象类型
type InfoType = {
name: string;
};
const info: InfoType = {
name: "hhh",
};
其实,还可以通过interface声明对象类型,用法类似 class;
有个接口规范,就是在接口前面加个大写字母I
class IInfoType {
name: string;
}
const info: IInfoType = {
name: "hhh",
};
索引类型
通过 interface 可以定义索引类型,使对象的 key-value 保持类型的统一;
interface IndexLang {
[index: number]: string;
}
const frontLang: IndexLang = {
0: "html",
1: "css",
2: "js",
3: "vue",
};
函数类型
以前可以通过 type 定义一个函数类型
type CalcFn = (n1: number, n2: number) => number;
function calc(num1: number, num2: number, calcFn: CalcFn) {
return calcFn(num1, num2);
}
const add: CalcFn = (num1, num2) => {
return num1 + num2;
};
calc(10, 20, add);
interface 也可以定义一个函数类型
interface CalcFn {
(n1: number, n2: number): number;
}
function calc(num1: number, num2: number, calcFn: CalcFn) {
return calcFn(num1, num2);
}
const add: CalcFn = (num1, num2) => {
return num1 + num2;
};
calc(10, 20, add);
接口继承
接口支持多继承,这是结合多个接口的一个方式;
interface ISwim {
swimming: () => void;
}
interface IFly {
flying: () => void;
}
interface IAction extends ISwim, IFly {}
const action: IAction = {
swimming() {},
flying() {},
};