JavaScript相关的类型注解系统

TypeScrip

类型系统

强类型与弱类型(类型安全方面)

非权威机构的定义

强类型

  • 函数的实参与形参的类型必须相同
  • 类型约束
  • 不允许隐式类型转换

弱类型

  • 函数的实参与形参的类型在语法上不必相同
  • 类型上约束少
  • 允许任意的数据隐式类型转换
  • JavaScript的TypeError是在运行时通过逻辑判断抛出的,而不是编译时就会抛出。

静态类型与动态类型(类型检查方面)

静态类型

  • 变量在声明时类型就是明确的,且声明后变量的类型不允许再修改
  • 通俗地说,静态表示的是看一眼就知道是什么

动态类型

  • 变量在运行时类型才能明确,且可以随意更改变量的类型
  • 变量没有类型,变量存放的值才有类型
  • 通俗地说,动态表示只有运行的时候才知道是什么

JavaScript自身类型系统的问题

JavaScript类型系统特征

  • 弱类型 + 动态类型
  • 缺少类型系统的可靠性

弱类型的问题

  • 类型异常的情况只有运行代码的时候才知道,导致不能及时发现问题
  • 没有类型约束导致功能出现问题,如函数返回值对于不同的参数类型是不同的。
  • 类型的隐式转换出现一些意料之外的问题

强类型的优势

  • 错误更早发现
  • 代码具备智能提示,编码更准确
  • 重构更可靠
  • 减少代码中不必要的类型判断

Flow静态类型检查方案

静态类型检查工具

  • 类型注解的方式标注变量的类型let a: number = 6; // : number即为类型注解
  • 可以通过Babel或Facebook官方的模块在生产环境中去除类型注解
  • 不要求所有的变量都进行类型注解,可以按需注解

Flow的原理

对代码进行了编译,使得在开发阶段就可以使用一些扩展语法。

Flow的使用

  • flow是一个node包,需要在使用前安装npm install flow-bin -D。这里使用了开发依赖来安装flow-bin包,为的是让flow跟随项目,让项目的其他开发人员了解需要使用flow来进行静态类型检查。也可以使用全局安装的形式,使用命令行来启动flow的检查。
  • 使用npx flow init命令生成.flowconfig配置文件
  • 在需要检查的JS文件开头添加// @flow标记或者更为复杂注解方式。
// index.js文件
// @flow
/**
* 这是另一种在开头添加标记的方式
* @flow
**/

function sum(a: number, b: number) {
  return a + b;
}
sum(100, 200);
// 在执行flow的时候,下面的调用会报错
sum('100', '200');
  • 执行npx flow来读取配置文件,执行flow服务。第一次执行的时候有点慢,是因为启动了一个后台服务,之后再执行flow时,就会很快的。

编译移除类型注解

flow-remove-types包

官方的移除注解方案

  • 安装npm install flow-remove-types -D
  • 运行npx flow-remove-types 目标文件路径 -d 生成文件路径
Babel + @babel/preset-flow插件
  • 安装npm install @babel/core @babel/cli @babel/preset-flow -D
  • 添加Babel配置文件.babelrc
// .babelrc文件
{
  "presets": ["@babel/preset-flow"]
}
  • 使用babel 目标文件路径 -d 生成文件路径启动编译
    以上的Flow静态检查都会把检查结果输入到控制台,不太方便。

Flow Language Support插件(VSCode)

安装vscode的Flow Language Support插件,可以在vscode开发过程中进行静态类型检查,将代码中的类型问题以红色波浪线显示出来。
Flow官网给出的所有编辑器下支持的插件,点击查看

Flow的类型推断

Flow也可以根据值的类型推断出存储该值的变量的类型,从而显示出类型问题。但尽量还是添加类型注解,增加代码可读性。

Flow中的类型注解方法

原始类型值
  • string:const a: string = 'foo';
  • number:const b: number = NaN; // NaN属性number类型
  • boolean:const c: boolean = false;
  • null:const d: null = null;
  • undefined:const e: void = undefined; // 要标注值为undefined类型时,需要用void
  • symbol:const f: symbol = Symbol('symbol type');
数组类型
  • const arr1: Array<number> = [1, 2, 3]; // 表示数组元素全部都是number类型
  • const arr2: number[] = [1, 2, 3];
  • const foo: [string, number] = ['foo', 6]; // 表示固定长度的数组,并指定元素类型,这里实际上表示了元组这种类型
对象类型
  • const obj1: { foo: string, bar: number } = { foo: 'foo', bar: 6 } //对属性值进行类型注解
  • const obj21: { foo?: string, bar: number } = { bar: 6 } // 可以在属性名后添加?表示该属性属于可选属性
  • const obj3: { [string]: string } = {}; obj3.key1 = 'value1'; obj3.key2 = 68; // 动态添加属性的情况下,可以先指定属性的类型
函数类型:对参数与返回值进行类型约束
  • 函数参数与返回值的类型注解
function square(n: number): number {
  return n * n;
}
// 没有返回值则标记为void
function foo(): void {
  console.log('void function');
}
  • 对函数作为值的情况下进行类型约束,使用箭头函数的方式来注解:(参数类型注解) => 返回值类型注解
function foo(callback: (string, number) => void) {
  callback('string', 66);
}
特殊类型
字面量类型

类型注解为某个字面量值const a: 'foo' = 'foo'; // 要求变量a只能存放值'foo'; 一般这样做意义不大,主要是配合联合类型来使用

联合类型
  • 联合类型用来约束变量只能存放约束范围内的值const type: 'success' | 'waring' | 'danger' = 'success'; // type变量只能存放'success'、'warning'、'danger'这三个值中的某一个
  • 联合类型也可以约束变量只能存放约束范围内的类型const b: string | number = 'foo'; // 变量b只能存放string类型或者number类型
MayBe类型

当变量可以是约束的类型,也可以是null或undefined时,使用?作为类型前缀来注解。

// ?number表示除了number类型外,可以是undefined或null
const gender: ?number = undefined;
mixed类型

所有类型的联合类型const a: mixed = 'foo';
mixed类型的意义在于变量不能在使用过程中随意更改自己的类型。一旦确定了是哪种类型,之后就必须是该类型。换句话说,mixed依然是强类型。

any类型

任意类型const b: any = 'foo';
any类型允许变量在使用过程中随意更改自己的类型,即弱类型。

类型别名(类型声明)

使用type关键字来声明一个类型(自定义一个类型,或者称为类型别名),然后可以使用该类型别名来做注解。

// type声明一个StringOrNumber的类型别名,然后可以使用该类型别名来做注解
type StringOrNumber = string | number;
const c: StringOrNumber = 66;

Flow相关参考资料

官方类型手册
第三方类型手册,该手册更加直观清晰。

Flow运行环境API

如浏览器环境或Node环境下提供的API,这些API所对应的类型声明文件。
这些API对应的类型声明文件点击这里

TypeScript语言规范与基本应用

TypeScript是JavaScript的超集

超集 = JavaScript + 类型系统 + ES6+
TypeScript 通过编译生成 JavaScript

缺点

  • 新的概念
  • 项目初期带来一些成本

基本使用

  • 安装:npm install typescript -D作为开发依赖安装,当然也可以全局安装。局部安装之后,就会出现/node_modules/bin/tsc文件,tsc作为TypeScript编译文件的命令。
  • 编译:使用npx tsc 源文件路径 -o 生成文件路径来将源文件编译为JavaScript,将ES6+语法生成ES5甚至ES3的语法。
  • 配置:使用配置文件来为编译做配置

配置文件:为编译项目做统一配置

  • 生成配置文件:使用npx tsc --init在项目根目录下生成tsconfig.json配置文件
  • 配置文件中的配置字段:“compilerOptions”
    • “target”:表示生成文件的ES语法
    • “module”:表示生成文件的模块语法,默认为CommonJS规范
    • “lib”:表示编译时使用的类型声明标准库。如果不开启时,默认与target字段相对应。即target如果是es5,则类型声明库使用es5的声明库,这样一来源代码中的ES6新增的API就会报错。当lib设置为[“es2015”]时,需要同时添加"DOM"声明库,即[“es2015”, “DOM”],因为开启了lib之后,屏蔽了默认使用的声明库,需要将浏览器环境下的API声明库再添加回来。
      • 标准库:内置对象所对应的声明文件
    • “outDir”:生成文件的路径
    • “rootDir”:源文件的路径
    • “sourceMap”:是否生成sourceMap
    • strict:是否使用严格模式类型检查。严格模式要求为每个变量明确指定类型,不允许类型推断。
    • strictNullChecks:是否使用严格模式检查null值
  • 只有在用tsc命令编译项目时才会读取配置文件,如果只是编译某个文件,则配置文件中的信息不会被读取使用。
  • 编译的时候回根据类型声明文件(包括标准库和自定义的类型声明文件)进行编译。

显示中文错误消息

  • 使用npx tsc --locale zh-CN命令让TypeScript在控制台显示中文错误消息。
  • 在vscode的settings中,找到TypeScript: Locale选项,设置为zh-CN,这样在vscode中显示的错误消息就会是中文的。

作用域问题

如果文件之间不是模块作用域或者函数作用域时,声明的变量会被编译到全局作用域,有可能会出现重复声明的报错问题。

TypeScript中的类型系统

TypeScript中注解原始类型值

基本与Flow一致。
不同点:

  • string、number和boolean在非严格模式下可以为null
  • symbol类型:注解为symbol时,当target不为es2015时,值如果是Symbol类型会有报错。
  • void类型:值可以是undefined或null(严格模式下不能为null)

TypeScript中的引用类型约束

object类型

泛指所有的非原始类型,或者说object类型指的是所有引用类型。

const foo: object = {}; // 值可以是对象、数组、函数等等

如果单纯指对象类型,则使用对象字面量来做类型注解或者接口interface。对象类型要求值与类型注解的shape完全一致。

const obj: { foo: number } = { foo: 66 }; // 这里的值必须是属性为foo且值为number类型的对象

数组类型

  • Array<T>Array泛型,如Array<string>
  • <T>[]泛型字面量,如number[]

元组类型

  • 元素数量和各个元素类型都明确的数组。
  • 使用字面量形式注解,如const tuple: [number, string] = [ 66, 'hello'];
  • 适合在一个函数中返回多个类型的值。如Map中的键值对
  • React中的Hook就使用了二元形式的元组类型

枚举类型

  • 给一组值进行额外的信息标注(让值有意义的描述)
  • 枚举并不只是类型,TypeScript中的枚举是有值的。
  • 有限的一组值。
  • 枚举类型会在编译时入侵源代码,也就是说,枚举类型会被加入到编译后的生成代码中,通过生成一个双向键值对的方式(双向键值对指的是通过键找到值,通过值找到键)
    使用enum关键字声明一个枚举类型的变量并初始化。
// 声明枚举类型PostStatus
enum PostStatus {
  // 注意这里使用=而不是:来赋值
  Draft = 0,
  Unpublished = 1,
  Published = 2
}
// 默认情况下,值是从0开始的自增长,所以以上的声明可以不写值
enum PostStatus {
  Draft, Unpublished, Published
}
// 枚举的值也可以是字符串,但这时候就需要明确的初始化了。
enum PostStatus {
  Draft = 'draft', Unpublished = 'unpublished', Published = 'published'
}
const post = {
  // 使用.符号来使用枚举类型的值
  status: PostStatus.Draft
}

如果不需要通过索引来找到值或者键,则建议使用常量枚举类型,在enum之前加入const。这样枚举类型代码则不会入侵到源代码中,生成的代码只会使用枚举的值。

const enum PostStatus {
  Draft, Unpublished, Published
}

函数类型

函数声明约束
// b为可选参数
function func1(a: number, b?: number, ...rest: number[]): string {
  return 'func1';
}
函数表达式约束
const func2: (a: number, b?number) => string = function(a: number, b?: number): string {
  return 'func2';
}

任意类型

使用any来注解类型,同样也是弱类型。

联合类型

  • 多种类型中的任意一个,使用 | 符号来连接多种类型
type Uni = string | number; // 定义类型别名Uni,string和number构成的联合类型

交叉类型

  • 多种类型的合并为一个类型,包含了所有类型的成员
type Uni = string & number; // 定义类型别名Uni, string和number构成的联合类型

隐式类型推断

根据代码中变量的使用情况来对变量的类型做推断。经过类型推断的变量如果再重新赋予其他类型的值,就会报错。
建议为变量明确类型,而不是使用隐式推断。

类型断言

  • 明确告诉TypeScript编译器某个变量的类型一定是某个类型,从而使得TypeScript不会因为类型不唯一而出现不正确的报错。
  • 使用as关键字来断言,或者使用<T>来断言(在JSX语法下这种方式会产生混淆,以为<>)
  • 类型断言不是类型转换,类型断言是编译过程中的,类型转换是运行时
const nums = [100, 101, 102];
const res = nums.find(i => i > 1); // 如果不使用断言,这里TypeScript会推断res类型为number | undefined,因为TypeScript并不会执行代码。实际上,这里的res一定是number。
const square = res * res; // 如果不进行类型断言,这里的res会报错,因为undefined类型不可以这样运算
const num1 = res as number; // as + 类型,断言
const num2 = <number>res; // <T>断言

接口Interface

  • 可以理解为一种规范,约束对象的结构(成员及成员类型)
  • 使用interface关键字来声明
  • 可选成员,在成员后添加?
  • 只读成员,在成员之前添加readonly
  • 动态成员,如缓存对象在运行时会动态添加成员
  • 同名接口会自动合并(类则不可以)
interface Post {
  title: string;
  content: string;
  subtitle?: string; // 可选成员
  readonly summary: string;
}
// 缓存对象中动态成员
interface cache {
  // 使用[]来表示未命名的成员以及成员名的类型
  [prop: string]: string
}
function printPost(post: Post) {
  console.log(post.title, post.content);
}

描述一类具体对象的抽象成员
TypeScript增强了class的相关语法

基本使用

  • 类内的属性成员需要先定义,不能动态添加;
class Person {
  // 需要先对成员进行类型注解
  name: string, // 可以在类成员定义时初始化,也可以在构造函数中初始化
  age: number
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  sayHi(msg: string): void {
    console.log(`I am ${this.name}, ${msg}`);
  }
}

访问修饰符

  • private:私有属性,只有类内部可以访问
  • protected:只有类和子类才可以访问
  • public:公共属性,类外部也可以访问
class Person {
  // 访问修饰符对成员的访问控制
  public name: string, // 可以在类成员定义时初始化,也可以在构造函数中初始化
  private age: number,
  protected gender: boolean
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
    this.gender = true;
  }
  sayHi(msg: string): void {
    console.log(`I am ${this.name}, ${msg}`);
  }
}

如果构造函数使用private修饰之后,则外部无法调用构造函数来创建实例。除非类暴露出静态方法来供外部调用。

只读属性

使用readonly关键字,跟在访问修饰符的后面,在成员名之前。

类与接口

接口是一种抽象的规范,类可以是抽象的规范,也可以是具体的实现。类可以使用implements关键字来表示对接口的实现。

// 定义一个接口,规范了该接口必须要有的成员,但成员是抽象的,不必具体实现
interface EatAndRun {
  eat(food: string): void;
  run(distance: number): void;
}
// 定义了一个Person类,该类具体实现了接口EatAndRun的规范
class Person implements EatAndRun {
  eat(food: string): void {
    console.log(`优雅地进餐: ${food}`);
  }
  run(distance: number): void {
    console.log(`直立行走: ${distance}`);
  }
}
// 定义了一个Animal类,该类也具体实现了接口EatAndRun的规范
class Animal implements EatAndRun {
  eat(food: string): void {
    console.log(`呼噜呼噜地吃: ${food}`);
  }
  run(distance: number): void {
    console.log(`爬行: ${distance}`);
  }
}

更合理的是,一个接口应该只规范一个成员

// 定义Eat接口
interface Eat {
  eat(food: string): void;
}
// 定义Run接口
interface Run {
  run(distance: number): void;
}
// 定义了一个Person类,该类具体实现了接口Eat、Run的规范
class Person implements Eat, Run {
  eat(food: string): void {
    console.log(`优雅地进餐: ${food}`);
  }
  run(distance: number): void {
    console.log(`直立行走: ${distance}`);
  }
}
// 定义了一个Animal类,该类也具体实现了接口Eat、Run的规范
class Animal implements Eat, Run {
  eat(food: string): void {
    console.log(`呼噜呼噜地吃: ${food}`);
  }
  run(distance: number): void {
    console.log(`爬行: ${distance}`);
  }
}
抽象类
  • 与接口相似,但可以有具体的实现。
  • 使用abstract关键字定义抽象类,抽象类只能继承,不能用来构造实例。
  • 抽象类中也可以使用abstract关键字定义抽象方法,子类需要去具体实现抽象方法。

泛型

  • 指的是声明时没有指定具体类型,只有在使用的时候才去具体指定类型。
  • 使用<>来指定具体类型,即类型断言
// 创建一个长度为length、元素为value的数组
function createNumberArray(length: number, value: number): number[] {
  // const arr = Array(length).fill(value); 这种方式下使用Array(length)得到的数组元素是any类型,在Array标准库声明文件中,使用的是Array<T>泛型,指的是在使用Array()方法时,再指定<T>中T的类型。
  const arr = Array<number>(length).fill(value);
  return arr;
}

可以在声明时使用泛型参数,等到调用时再指定泛型参数的类型。这样可以更大程度复用代码。

// 创建一个长度为length、元素为value的数组,这里数组的元素是任意类型的,只有调用时传入具体类型。
function createArray<T>(length: number, value: <T>): <T>[] {
  // const arr = Array(length).fill(value); 这种方式下使用Array(length)得到的数组元素是any类型,在Array标准库声明文件中,使用的是Array<T>泛型,指的是在使用Array()方法时,再指定<T>中T的类型。
  const arr = Array<T>(length).fill(value);
  return arr;
}
// 调用时指定泛型参数为string
const arr = createArray<string>(3, 'hello');

类型声明及类型声明文件

  • 使用declare关键字来对用到的函数、类等进行类型声明。一般是为了对没有类型声明的第三方模块进行补充声明。
  • 很多第三方模块在TypeScript社区都已经有类型声明文件,只要安装就可以了,注意是开发依赖。npm install @types/模块名 -D。一般类型声明文件是.d.ts后缀名文件,命名以@types/开头。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章