超细致的TypeScript入门与实战

 

本文是向大家介绍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

可以通过以下链接搜索需要的声明文件。

TypeScript: Search for typed packages

如果第三方库没有提供声明文件,我们就需要自己书写声明文件。如何书写声明文件?此处略过,感兴趣的话可以后期尝试。

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 插件,快速自动化获取 YApiSwagger 的接口定义,生成 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.参考文献

官方手册:TypeScript: Handbook - Basic Types

入门教程:什么是 TypeScript · TypeScript 入门教程


版权声明:本文为weixin_43805705原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。