本文是向大家介绍TypeScript基础知识及用法,帮助大家快速了解,在前端项目中使用这门技术。TypeScript的类型推断跟 VS Code 的良好搭配让代码效率有了极大提升。静态类型检测,会提示一些潜在的问题,使得开发者代码更严谨,低级错误早发现早解决。同时TypeScript类型定义使得代码的可读性增强,统一规范,可有效降低项目维护成本。
1.TypeScript简介
什么是TypeScript?
TypeScript是微软开发的一个开源的编程语言,通过在JavaScript的基础上添加静态类型定义构建而成。TypeScript通过TypeScript编译器或Babel转译为JavaScript代码,可运行在任何浏览器,任何操作系统。——来自TypeScript官方
1.1 TypeScript类型系统
从TypeScript的名字就可以看出来,「类型」是其最核心的特性。 我们知道,JavaScript是一门非常灵活的编程语言:
它没有类型约束,一个变量可能初始化时是字符串,过一会儿又被赋值为数字。
由于隐式类型转换的存在,有的变量的类型很难在运行前就确定。
基于原型的面向对象编程,使得原型上的属性或方法可以在运行时被修改。
函数可以赋值给变量,也可以当作参数或返回值。
这种灵活性就像一把双刃剑,一方面使得JavaScript蓬勃发展,无所不能。另一方面也使得它的代码质量参差不齐, 维护成本高,运行时错误多。而TypeScript的类型系统,在很大程度上弥补了JavaScript的缺点。
1.2 TypeScript 是静态类型
类型系统按照「类型检查的时机」来分类,可以分为动态类型和静态类型。
动态类型是指在运行时才会进行类型检查,这种语言的类型错误往往会导致运行时错误。JavaScript 是一门解释型语言,没有编译阶段,所以它是动态类型。
静态类型是指编译阶段就能确定每个变量的类型,这种语言的类型错误往往会导致语法错误。TypeScript 在运行前需要先编译为 JavaScript,而在编译阶段就会进行类型检查,所以 TypeScript 是静态类型。
1.3 TypeScript 是弱类型
类型系统按照「是否允许隐式类型转换」来分类,可以分为强类型和弱类型。
TypeScript 是完全兼容 JavaScript 的,它不会修改 JavaScript 运行时的特性,所以它们都是弱类型。
在完整保留 JavaScript 运行时行为的基础上,通过引入静态类型系统来提高代码的可维护性,减少可能出现的 bug。以下这段代码不管是在 JavaScript 中还是在 TypeScript 中都是可以正常运行的:
console.log(1 + '1'); // 打印出字符串 '11'
1.4 适用于任何规模
TypeScript 非常适用于大型项目——这是显而易见的,类型系统可以为大型项目带来更高的可维护性,以及更少的 bug。
在中小型项目中推行 TypeScript 的最大障碍就是认为使用 TypeScript 需要写额外的代码,降低开发效率。但事实上,由于有[类型推论][],大部分类型都不需要手动声明了。相反,TypeScript 增强了编辑器(IDE)的功能,包括代码补全、接口提示、跳转到定义、代码重构等,这在很大程度上提高了开发效率。而且 TypeScript 有近百个[编译选项][],如果你认为类型检查过于严格,那么可以通过修改编译选项来降低类型检查的标准。
TypeScript 还可以和 JavaScript 共存。这意味着如果你有一个使用 JavaScript 开发的旧项目,又想使用 TypeScript 的特性,那么你不需要急着把整个项目都迁移到 TypeScript,你可以使用 TypeScript 编写新文件,然后在后续更迭中逐步迁移旧文件。如果一些 JavaScript 文件的迁移成本太高,TypeScript 也提供了一个方案,可以让你在不修改 JavaScript 文件的前提下,编写一个[类型声明文件][],实现旧项目的渐进式迁移。
1.5 安装TypeScript
TypeScript 的命令行工具安装方法如下:
npm install -g typescript
编译文件:
tsc hello.ts
TypeScript 最大的优势之一便是增强了编辑器和 IDE 的功能,包括代码补全、接口提示、跳转到定义、重构等。主流的编辑器都支持 TypeScript,就连VS Code也是基于TypeScript开发的。所以VS Code对TypeScript的支持也是非常好的,强烈推荐大家使用起来。
2.TypeScript类型
2.1 基本类型
2.1.1 原始数据类型
原始数据类型包括布尔值boolean、数值number、字符串string、undefined、null、void。
还有ES6 中的新类型 Symbol 和 ES10 中的新类型 BigInt。(最后两种目前运用的比较少,暂不做介绍)
JavaScript中没有空值(Void)的概念,在TypeScript中,可以用void表示没有任何返回值的函数。
undefined、null、void三种的区别是,undefined和null是所有类型的子类型。也就是说undefined和null类型的变量,可以复制给所有类型的变量。而void类型的变量不能赋值给其他类型的变量。
// 1-布尔值
let isDone: boolean = false;
// 2-数值
let dogAge: number = 3;
// 3-字符串
let dogName: string = 'Strawberry';
// 4-空值
function alertName(): void {
alert(`My dog's name is ${dogName}`);
}
// 5-undefined、null、void三种的区别
let a1: undefined = undefined;
let a2: null = null;
let a3: void;
let b1: number = undefined // ts校验通过
let b2: number = a2 // ts校验通过
let b3: number = a3 // ts报错:不能将类型“void”分配给类型“number”。
2.1.2 any类型和unknown类型
any 和 unknown 在 TypeScript 中是所谓的“顶级类型”。
top type [...]是 通用(universal) 类型,有时也称为 通用超类型,因为在任何给定类型系统中,所有其他类型都是子类型[...]。通常,类型是包含了其相关类型系统中所有可能的[值]的类型。----引用自 Wikipedia:
如果一个值的类型为 any,那么我们就可以用它做任何事。任何类型的值都可以赋值给 any 类型。类型 any 也可被可赋值给每一种类型。
unknown 类型是 any 的类型安全版本。每当你想使用 any 时,应该先试着用 unknown。在 any 允许我们做任何事的地方,unknown 的限制则大得多。在对 unknown 类型的值执行任何操作之前,必须先通过【类型断言】、【相等判断】、【类型防护】、【断言函数】进行限定其类型,否则会ts异常(后续章节会提到)。
使用 any,我们将会失去通常由 TypeScript 的静态类型系统所给予的所有保护。因此,如果我们无法使用更具体的类型或 unknown,则只能将其用作最后的手段。
// demo-1 原始类型都可以赋值给any类型的变量
let age: number = 18;
let any_word: any = 'hello';
// demo-2 在任意值上访问任何属性都是允许的
let anyThing: any = 'Tom';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);
// demo-3 变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型
let something;
something = 'seven';
something = 7;
something.setName('Tom');
// demo-4 any和unknown区别
let d1: any = 0
d1.money = 100 // ts校验通过
let d2: unknown = 0
d2.money = 100 // ts报错:unknown类型不存在money属性。
2.1.3 类型推论
TypeScript 会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查:
// demo-1 myFavoriteNumber3类型?
let myFavoriteNumber3: any = 'seven'; // 直接定义any类型
myFavoriteNumber3 = 7;
// demo-2 myFavoriteNumber4类型?
let myFavoriteNumber4; // ts推测为any类型
myFavoriteNumber4 = 'seven';
myFavoriteNumber4 = 7;
2.1.4 联合类型
联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类型:
let myFavoriteNumber5: string | number;
myFavoriteNumber5 = 'seven';
console.log(myFavoriteNumber5.length); // 5
myFavoriteNumber5 = 7;
console.log(myFavoriteNumber5.length); // ts报错:number类型不存在length属性
2.1.5 对象类型
声明对象类型,经常使用类型别名type和接口interface,二者在使用上的区别:
在类class的类型定义中我们推荐使用接口interface,因为接口interface可以被一个类class实现(implements),但是类型别名type不但不能被extends和implements,就连自己也不能extends和implements其它类型。
在定义简单类型、联合类型、交叉类型、元组时我们用类型别名type来做,并且它和typeof能够天然的结合在一起使用。
// 方式1-接口定义
interface Person {
name: string;
age: number;
}
// 方式2-类型别名定义
type Person = {
name: string;
age: number;
}
let tom: Person = {
name: 'Tom',
age: 25
};
2.1.6 数组类型
常用声明方式:
【类型+方括号】表示方法 string[]
数组泛型 Array<T>
// demo-1 ts只允许数组中包括一种数据类型的值
let arr1: number[] = [1, 2, 3]
let arr2: Array<number> = [1, 2, 3]
// demo-2 如果想为数组添加不同类型的值,需要使用联合类型
let arr3: Array<number | string> = ['aa', 2, 3] // 数组可以同时包括数值和字符串
let arr4: number[] | string[] = [1, 2, 3] // 数组只能全是数值组成,或者全字符串组成
arr4 = ['aa', 'bb']
arr4 = ['aa', 2, 3] // ts报错:赋值类型与定义不一
2.1.7 函数类型
根据JavaScript中定义函数的2种方式:函数声明(Function Declaration)和函数表达式(Function Expression),函数类型写法如下:
// 1. 函数声明(Function Declaration)
function add1(x: number, y: number): number {
return x + y
}
add1(1, 2);
// 2. 函数表达式(Function Expression)
let add2 = (x: number, y: number): number => {
return x + y
}
add2(1, 2);
// 可选参数必须接在必需参数后面
function add3(x: number, y?: number): void {
console.log(x, y)
}
add3(3)
2.1.8 类型断言
主要用于当 TypeScript 推断出来类型并不满足当前需求时,TypeScript 允许开发者覆盖它的推断,可以用来手动指定一个值的类型。类型断言是一个编译时语法,不涉及运行时。
类型断言的常见用途有以下几种:
联合类型可以被断言为其中一个类型
父类可以被断言为子类
任何类型都可以被断言为 any
any 可以被断言为任何类型
要使得 A 能够被断言为 B,只需要 A 兼容 B 或 B 兼容 A 即可
联合类型可以被断言为其中一个类型,类型断言只能够欺骗 TS 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误。
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function swim(animal: Cat | Fish) {
(animal as Fish).swim(); // 类型断言,ts校验通过
}
const tom: Cat = {
name: 'Tom',
run() { console.log('run') }
};
swim(tom);
// 编译时不会报错,但在运行时会报错 Uncaught TypeError: animal.swim is not a function`
类型断言不是类型转换,它不会真的影响到变量的类型。
// 类型断言
function toBoolean1(something: any): boolean {
return something as boolean;
}
toBoolean1(1); // 返回值为 1
// 类型转换
function toBoolean2(something: any): boolean {
return Boolean(something);
}
toBoolean2(1); // 返回值为 true
2.1.9 声明文件
当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
声明文件必需以 .d.ts 为后缀,声明文件示例:
// src/jQuery.d.ts
declare var jQuery: (selector: string) => any;
推荐使用 @types 统一管理第三方库的声明文件。@types 的使用方式很简单,直接用 npm 安装对应的声明模块即可,以 jQuery 举例: npm install @types/jquery --save-dev
可以通过以下链接搜索需要的声明文件。
https://www.typescriptlang.org/dt/search?search=
如果第三方库没有提供声明文件,我们就需要自己书写声明文件。如何书写声明文件?此处略过,感兴趣的话可以后期尝试。
2.1.10 内置对象
1.ECMAScript 标准提供的内置对象有:
Boolean、Error、Date、RegExp 等。
更多的内置对象,可以查看 MDN 的文档。
2.DOM 和 BOM 提供的内置对象有:
Document、HTMLElement、Event、NodeList 等。
而他们的定义文件,则在 TypeScript 核心库的定义文件中。
3.TypeScript 核心库的定义文件中定义了所有浏览器环境需要用到的类型,并且是预置在 TypeScript 中的。
注意,TypeScript 核心库的定义中不包含 Node.js 部分。
4.用 TypeScript 写 Node.js
Node.js 不是内置对象的一部分,如果想用 TypeScript 写 Node.js,则需要引入第三方声明文件:
npm install @types/node --save-dev
2.2 高级类型
2.2.1 Type类型别名
类型别名就是给类型起一个新名字。用法类似于接口interface,类型别名常用于原始类型、联合类型,元祖类型等其他必须手动编写的类型,示例:
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
type Animal = Cat | Fish;
2.2.2 字面量类型
字符串字面量类型用来约束取值只能是某几个字符串中的一个。
// 也可以直接使用字面量进行类型声明
// a只能被赋值为10 不能被赋值为其他值 类似常量 很少使用
let a: 10;
a = 10;
// 字面量形式一般用于或的形式较多
// 可以使用 | 来连接多个类型(类似联合类型)
let b: "male" | "female";
b = "male";
b = "female";
2.2.3 元祖
数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。
元组起源于函数编程语言(如 F#),这些语言中会频繁使用元组。
// 定义一对值分别为 string 和 number 的元组
let Jerry: [string, number] = ['Jerry', 18];
// 当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型
Jerry.push('male'); // ts校验通过
Jerry.push(true); // ts报错:true和"string|number"类型不匹配
2.2.4 枚举
// demo-1 简单枚举:枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射
enum Days { Sun, Mon, Tue, Wed, Thu, Fri, Sat };
console.log(Days["Sun"] === 0); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true
console.log(Days[0] === "Sun"); // true
console.log(Days[1] === "Mon"); // true
console.log(Days[2] === "Tue"); // true
console.log(Days[6] === "Sat"); // true
// demo-2 手动赋值:未手动赋值的枚举项会接着上一个枚举项递增,需要避免覆盖
enum DaysNew { Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat };
console.log(DaysNew["Sun"] === 7); // true
console.log(DaysNew["Mon"] === 1); // true
console.log(DaysNew["Tue"] === 2); // true
console.log(DaysNew["Sat"] === 6); // true
2.2.5 类
TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 public、private 和 protected。
public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的
private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的
给类加上 TypeScript 的类型很简单,与接口类似:
class Animal {
public name: string; // 思考1:此处改为private后运行结果?
public constructor(name: string) {
this.name = name;
}
public sayHi(): string { // 思考2:此处改为private后运行结果?
return `My name is ${this.name}`;
}
}
let a: Animal = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack
「构造函数」前加修饰符的区别:
当构造函数修饰为 private 时,该类不允许被继承或者实例化
当构造函数修饰为 protected 时,该类只允许被继承
2.2.6 类与接口
interface Alarm {
alert(): void;
}
interface Light {
lightOn(): void;
lightOff(): void;
}
// demo-1 接口继承接口
interface LightableAlarm extends Light {
lightOn(): void;
lightOff(): void;
}
// demo-2 类继承接口
class Car implements Alarm, Light {
alert() {
console.log('Car alert');
}
lightOn() {
console.log('Car light on');
}
lightOff() {
console.log('Car light off');
}
}
// demo-3 接口继承类
interface LittleCar extends Car {
console(): void;
}
let car1: LittleCar = {
alert() {
console.log('Little Car alert');
},
lightOn: function (): void {
console.log('Little Car light on');
},
lightOff: function (): void {
console.log('Little Car light off');
},
console: function (): void {
console.log('Little Car');
},
}
2.2.7 泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
示例:Array<any> 允许数组的每一项都为任意类型。但是我们预期的是,数组中每一项都应该是输入的 value 的类型。
function createArray<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
2.2.8 声明合并
如果定义了两个相同名字的函数、接口或类,那么它们会合并成一个类型。
合并的属性的类型必须是唯一的,如果出现重复,类型不一致会报错。
interface Alarm {
price: number;
}
interface Alarm {
weight: number;
}
// 合并后
interface Alarm {
price: number;
weight: number;
}
interface Alarm {
price: string; // ts报错:类型不一致
weight: number;
}
3.实战:案例分享
3.1 定义提示
鼠标放上UserInfo都会有相应的定义提示
3.2 定义跳转
在其他页面运用到UserInfo定义时,鼠标放上能快速看到定义类型和注释,点击能直接跳转定义代码
3.3 代码静态检测
根据ts定义自动检测代码,发现错误实时提示,鼠标放上会给出详细信息,并提供快速修复功能。
例如:截图中username没有用驼峰式书写,跟原定义不一致。点击即可自动修复。
3.4 接口定义快速引入项目
推荐安装yapi-to-typescript 插件,快速自动化获取 YApi 或 Swagger 的接口定义,生成 TypeScript 或 JavaScript 的接口类型及其请求函数代码。
插件安装方法:
npm i yapi-to-typescript
生成定义代码命令:
npx ytt
导出模块接口定义步骤:
1.categories中id获取方式:打开yapi项目 --> 点开分类 --> 复制浏览器地址栏 /api/cat_ 后面的数字。
2.yapi上查看项目token方法:
3.ytt.config.ts配置文件参考:
import { defineConfig } from 'yapi-to-typescript';
/* yapi获取ts定义 */
export default defineConfig([
{
serverUrl: 'http://XX.XX.XX.XX:XX', // yapi项目地址
typesOnly: true, // 只生产ts定义
target: 'typescript',
reactHooks: {
enabled: false,
},
prodEnvName: 'production',
outputFilePath: 'api/index.ts', // 定义输出目录
requestFunctionFilePath: 'api/request.ts',
dataKey: 'data',
projects: [
{
// yapi项目token,存在有效期限制
token: 'XXXXXXXXXXXXxx',
categories: [
{
id: 123, // 获取方式:打开项目 --> 点开分类 --> 复制浏览器地址栏 /api/cat_123后面的数字
getRequestFunctionName(interfaceInfo, changeCase) {
return changeCase.camelCase(interfaceInfo.parsedPath.name);
},
},
],
},
],
},
]);
4.自动生成定义截图:
4.参考文献
官方手册:https://www.typescriptlang.org/docs/handbook/basic-types.html
入门教程:http://ts.xcatliu.com/introduction/what-is-typescript.html