带你了解类型系统以及flow和typescript的基本使用

Study Notes

本博主会持续更新各种前端的技术,如果各位道友喜欢,可以关注、收藏、点赞下本博主的文章。

类型系统

强类型语言

强类型指的是程序中表达的任何对象所从属的类型都必须能在编译时刻确定。

对于强类型语言,一个变量不经过强制转换,它永远是这个数据类型,不允许隐式的类型转换。

假设定义了一个 double 类型变量 a,不经过强制类型转换那么程序 int b = a 是无法通过编译。

// 编译失败
double a;
int b = a;

强类型的优点

  • 编译时刻能检查出错误的类型匹配,以提高程序的安全性;
  • 可以根据对象类型优化相应运算,以提高目标代码的质量;
  • 重构更牢靠;
  • 减少运行时不必要的类型判断。

弱类型语言

弱类型语言允许变量类型的隐式转换,允许强制类型转换等,如字符串和数值可以自动转化

let a = '100';
let b = 50;
console.log(a - b);
// 50 将a隐式转换为Number
console.log(a + b);
// 10050 将b隐式转换为String

静态类型

静态类型语言中,变量的类型必须先声明,即在创建的那一刻就已经确定好变量的类型,而后的使用中,你只能将这一指定类型的数据赋值给变量。如果强行将其他不相干类型的数据赋值给它,就会引发错误。

动态类型

动态类型语言中,变量的类型可以随时改变。

Flow

Flow 是 JavaScript 的静态类型检查器

安装

安装和配置项目的流程

安装编译器

首先,您需要配置一个编译器以剥离 Flow 类型。您可以在 Babel 和 flow-remove-types 之间进行选择。

这边以 Babel 为例:

Babel 是 JavaScript 代码的编译器,具有对 Flow 的支持。Babel 可以将关于 Flow 代码剔除。

首先安装@babel/core,@babel/cli 并@babel/preset-flow 使用 Yarn 或 npm。

npm install --save-dev @babel/core @babel/cli @babel/preset-flow

接下来,你需要在你的项目的根文件下创建一个.babelrc。

{
  "presets": ["@babel/preset-flow"]
}

剔除命令运行

babel 输入需剔除的文件或文件夹路径 -d 输出文件夹

配置流程

安装 flow-bin

npm install --save-dev flow-bin

将"flow"脚本添加到您的 package.json:

{
  "scripts": {
    "flow": "flow"
  }
}

首次安装后,需要先初始化

npm run flow init

init 之后,运行 flow

npm run flow

使用

Type Annotations(类型注解)

/**
 * Type Annotations(类型注解)
 * flow
 */
// 参数添加类型注解
function add(x: number, y: number) {
  return x + y;
}

// 正确
add(100, 100);
// 报错
// add('100', 100);

// 声明基本类型数据时添加类型注解
let num: number = 100; // 正确
// num = '100'; // 报错

// 声明函数时添加类型注解
function sum(): number {
  return 100; // 只能返回number类型数据
  // return '100'; // 报错
}

Primitive Types(原始类型)

  • Booleans
  • Strings
  • Numbers
  • null
  • undefined (void in Flow types)
  • Symbols (new in ECMAScript 2015)
/**
 * Primitive Types(原始类型)
 * @flow
 */
const bol: boolean = true; // false Boolean(0) Boolean(1)

const str: string = 'abs';

const nums: number = 1; // 3.14 NaN Infinity

const emt: null = null;

const un: void = undefined;

const syb: symbol = Symbol(); // Symbol.isConcatSpreadable

Literal Types(文字类型)

Flow 具有文字值的原始类型,但也可以将文字值用作类型。

例如,number 除了接受类型,我们可以只接受文字值 2。

/**
 * Literal Types(文字类型)
 * @flow
 */
function acceptsTwo(value: 2) {
  // ...
}

acceptsTwo(2); // Works!
// $ExpectError
acceptsTwo(3); // Error!
// $ExpectError
acceptsTwo('2'); // Error!

将它们与联合类型一起使用

/**
 * Literal Types(文字类型)
 * @flow
 */
function getColor(name: 'success' | 'warning' | 'danger') {
  switch (name) {
    case 'success':
      return 'green';
    case 'warning':
      return 'yellow';
    case 'danger':
      return 'red';
  }
}

getColor('success'); // Works!
getColor('danger'); // Works!
// $ExpectError
getColor('error'); // Error!

Mixed Types(混合类型)

mixed 将接受任何类型的值。字符串,数字,对象,函数等。

/**
 * Mixed Types(混合类型)
 * @flow
 */
function stringify(value: mixed) {
  // ...
}

stringify('foo');
stringify(3.14);
stringify(null);
stringify({});

当您尝试使用 mixed 类型的值时,必须首先弄清楚实际的类型是什么,否则最终会出错。

/**
 * Mixed Types(混合类型)
 * @flow
 */
function stringify(value: mixed) {
  return '' + value; // Error!
}

stringify('foo');

通过 typeof 来确保该值是某种类型

/**
 * Mixed Types(混合类型)
 * @flow
 */
function stringify(value: mixed) {
  if (typeof value === 'string') {
    return '' + value; // Works!
  } else {
    return '';
  }
}

stringify('foo');

Any Types(任何类型)

使用any是完全不安全的,应尽可能避免使用。

/**
 * Any Types(任何类型)
 * @flow
 */
function division(one: any, two: any): number {
  return one / two;
}

division(1, 2); // Works.
division('1', '2'); // Works.
division({}, []); // Works.

Maybe Types(可能类型)

使用 Flow 可以将 Maybe 类型用于这些值。可能类型可以与其他任何类型一起使用,只需在其前面加上一个问号即可,例如?number 某种修饰符。

例如:?number 就意味着 number,null 或 undefined。

/**
 * Maybe Types(可能类型)
 * @flow
 */
function acceptsMaybeNumber(value: ?number) {
  // ...
}

acceptsMaybeNumber(42); // Works!
acceptsMaybeNumber(); // Works!
acceptsMaybeNumber(undefined); // Works!
acceptsMaybeNumber(null); // Works!
acceptsMaybeNumber('42'); // Error!

Function Types(函数类型)

function concat(a: string, b: string): string {
  return a + b;
}

concat('foo', 'bar'); // Works!
// $ExpectError
concat(true, false); // Error!

function method(func: (...args: Array<any>) => any) {
  func(1, 2); // Works.
  func('1', '2'); // Works.
  func({}, []); // Works.
}

method(function (a: number, b: number) {
  // ...
});

Object Types(对象类型)

/**
 * Object Types(对象类型)
 * @flow
 */
let obj1: { foo: boolean } = { foo: true }; // Works.
obj1.bar = true; // Error!
obj1.foo = 'hello'; // Error!

let obj2: {
  foo: number,
  bar: boolean,
  baz: string,
} = {
  foo: 1,
  bar: true,
  baz: 'three',
}; // Works.

let obj3: { foo: string, bar: boolean };
obj3 = { foo: 'foo', bar: true };
obj3 = { foo: 'foo' };

更多类型查看types

TypeScript

TypeScript 是 JavaScript 类型的超集,可编译为普通 JavaScript,支持 ECMAScript 6 标准,可运行在任何浏览器上。

TypeScript 是渐进式的

目前官网上已更新到 TypeScript 4.0 ,而中文官网更新到 TypeScript 3.1

TypeScript(官网)

TypeScript(中文网)

安装

这里是针对项目,不进行全局安装

npm i typescript -D

使用 ts-node 可以直接在 node 环境下运行 ts 文件,方便开发环境测试

npm i ts-node -D

运行

ts-node 文件路径

简单使用

const test = (name: string) => console.log(`hello ${name}`);

test('typescript');

编译 ts 代码,生成一个 index.js 文件,并被转换为 es5

tsc index

index.js

var test = function (name) {
  return console.log('hello ' + name);
};
test('typescript');

配置

生成配置文件 tsconfig.json

tsc --init

具体配置可以查看Compiler Options(编译选项)

操作手册

Basic Types(基础类型)

为了使程序有用,我们需要能够使用一些最简单的数据单元:数字,字符串,结构,布尔值等。 在 TypeScript 中,我们支持与 JavaScript 中期望的类型几乎相同的类型,并抛出了方便的枚举类型以帮助处理问题。

Boolean(布尔类型)

let isDone: boolean = true;

Number(数字)

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;

String(字符串)

let str: string = 'bob';
str = 'smith';
str = `smith${str}`;

Array(数组)

let list: number[] = [1, 2, 3];
let list1: Array<number> = [1, 2, 3];

Tuple(元组)

let x: [string, number];
x = ['hello', 10]; // OK
// x = [10, 'hello']; // Error

Enum(枚举)

Enum 类型是对 JavaScript 标准数据类型的一个补充。

像 C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。

enum Color {
  Red = 8,
  Green,
  Blue,
} // 默认0,1,2
let c: Color = Color.Green;
let cName: string = Color[9];
console.log(c);
console.log(cName);
// 9
// Green

Any(任何类型)

let notSure: any = 4;
notSure = 'maybe a string instead';
notSure = false;
notSure = 1;

Void

某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void:

function warnUser(): void {
  console.log('This is my warning message');
}

声明一个 void 类型的变量没有什么大用,因为你只能为它赋予 undefined 和 null:

let unusable: void = undefined;

Null and Undefined

TypeScript 里,undefined 和 null 两者各自有自己的类型分别叫做 undefined 和 null。 和 void 相似,它们的本身的类型用处不是很大

let u: undefined = undefined;
let n: null = null;

Never

never 类型表示的是那些永不存在的值的类型。 例如, never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never 类型,当它们被永不为真的类型保护所约束时。

never 类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是 never 的子类型或可以赋值给 never 类型(除了 never 本身之外)。 即使 any 也不可以赋值给 never。

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
  throw new Error(message);
}

// 推断的返回值类型为never
function fail() {
  return error('Something failed');
}

Object(对象类型)

object 表示非原始类型,也就是除 number,string,boolean,symbol,null 或 undefined 之外的类型。

declare function create(o: object | null): void;

create({ prop: 0 }); // OK
create(function () {}); // OK
create([1]); // OK
create(null); // OK
// create(42); // Error
// create('string'); // Error
// create(false); // Error
// create(undefined); // Error

函数

function add(x: number, y: number): number {
  return x + y;
}
const division = (x: number, y: number): number => {
  return x / y;
};
// 书写完整函数类型
let myAdd: (baseValue: number, increment: number) => number = function (
  x: number,
  y: number,
): number {
  return x + y;
};
const myDivision: (baseValue: number, increment: number) => number = (
  x: number,
  y: number,
): number => {
  return x / y;
};

隐式类型推断

let age = 18; // typescript会隐式类型推断其为number
let name = '18'; // typescript会隐式类型推断其为string
let className; // typescript会隐式类型推断其为any

Type assertions(类型断言)

有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript 会假设你,程序员,已经进行了必须的检查。

类型断言有两种形式。 其一是“尖括号”语法:

let someValue: any = 'this is a string';

let strLength: number = (<string>someValue).length;

另一个为 as 语法:

let someValue: any = 'this is a string';

let strLength: number = (someValue as string).length;

Interfaces(接口)

interface LabelledValue {
  label: string;
}

function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}

let myObj = { size: 10, label: 'Size 10 Object' };
printLabel(myObj);

可选属性

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  let newSquare = { color: 'white', area: 100 };
  if (config.color) {
    newSquare.color = config.color;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}

let mySquare = createSquare({ color: 'black' });
console.log(mySquare);

只读属性

interface Point {
  readonly x: number;
  readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
// p1.x = 5; // error!

Class(类)

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  getName(): string {
    return this.name;
  }
}

修饰符

在 TypeScript 里,成员都默认为 public

  • public

可以自由的访问程序里定义的成员

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  getName(): string {
    return this.name;
  }
}
console.log(new Person('zs').getName()); //zs
  • private

当成员被标记成 private 时,它就只能在类的内部访问。

class Person {
  private name: string;
  constructor(name: string) {
    this.name = name;
  }
  getName(): string {
    return this.name;
  }
}
new Person('zs').name; // 错误: 'name' 是私有的.
  • protected

protected 修饰符与 private 修饰符的行为很相似,但有一点不同, protected 成员在派生类中仍然可以访问。

class Person {
  private name: string;
  protected age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}
class School extends Person {
  constructor(name: string, age: number) {
    super(name, age);
  }
  getName(): string {
    return this.name; // error,不能被访问
  }
  getAge(): number {
    return this.age; // OK,可以被访问
  }
}

readonly(只读)

你可以使用 readonly 关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化

class Octopus {
  readonly name: string;
  readonly numberOfLegs: number = 8;
  constructor(theName: string) {
    this.name = theName;
  }
}
let dad = new Octopus('Man with the 8 strong legs');
dad.name = 'Man with the 3-piece suit'; // 错误! name 是只读的.

类实现接口

与 C#或 Java 里接口的基本作用一样,TypeScript 也能够用它来明确的强制一个类去符合某种契约。

interface Run {
  run(): void;
}
class Car implements Run {
  run(): void {
    console.log('我会跑...');
  }
}
new Car().run();

抽象类

抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法。

abstract class Animal {
  run(): void {
    console.log('我会跑...');
  }
}

class Doc extends Animal {
  eat(): void {
    console.log('我会吃...');
  }
}

let doc = new Doc();
doc.eat();
doc.run();

Generics(泛型)

软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像 C#和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

// 普通函数
function createArray<T>(...args: T[]): T[] {
  return args;
}

console.log(createArray<number>(1, 2, 3));
console.log(createArray<string>('jack', 'tom'));

// 箭头函数
const createArrayArrow = <T>(...args: T[]): T[] => {
  return args;
};

console.log(createArrayArrow<number>(1, 2, 3));
console.log(createArrayArrow<string>('jack', 'tom'));

更多

定义组件的几种不同方式

使用 Options APIs

  • 组件仍然可以使用以前的方式定义(导出组件选项对象,或者使用 Vue.extend())
  • 但是当我们导出的是一个普通的对象,此时 TypeScript 无法推断出对应的类型,
  • 至于 VSCode 可以推断出类型成员的原因是因为我们使用了 Vue 插件,
  • 这个插件明确知道我们这里导出的是一个 Vue 对象。
  • 所以我们必须使用 Vue.extend() 方法确保 TypeScript 能够有正常的类型推断
import Vue from 'vue';

export default Vue.extend({
  name: 'Button',
  data() {
    return {
      count: 1,
    };
  },
  methods: {
    increment() {
      this.count++;
    },
  },
});

使用 Class APIs

在 TypeScript 下,Vue 的组件可以使用一个继承自 Vue 类型的子类表示,这种类型需要使用 Component 装饰器去修饰

装饰器是 ES 草案中的一个新特性,提供一种更好的面向切面编程的体验,不过这个草案最近有可能发生重大调整,所以个人并不推荐。

装饰器函数接收的参数就是以前的组件选项对象(data、props、methods 之类)

import Vue from 'vue';
import Component from 'vue-class-component';

@Component({
  props: {
    size: String,
  },
})
export default class Button extends Vue {
  private count: number = 1;
  private text: string = 'Click me';

  get content() {
    return `${this.text} ${this.count}`;
  }

  increment() {
    this.count++;
  }

  mounted() {
    console.log('button is mounted');
  }
}
  • Data: 使用类的实例属性声明
  • Method: 使用类的实例方法声明
  • Computed: 使用 Getter 属性声明
  • 生命周期: 使用类的实例方法声明

其它特性:例如 components, props, filters, directives 之类的,则需要使用修饰器参数传入

使用这种 class 风格的组件声明方式并没有什么特别的好处,只是为了提供给开发者多种编码风格的选择性

使用 Class APIs + vue-property-decorator

这种方式继续放大了 class 这种组件定义方法。

import { Vue, Component, Prop } from 'vue-property-decorator'

@Component
export default class Button extends Vue {
  private count: number = 1
  private text: string = 'Click me'
  @Prop() readonly size?: string

  get content () {
    return `${this.text} ${this.count}`
  }

  increment () {
    this.count++
  }

  mounted () {
    console.log('button is mounted')
  }
}

个人最佳实践

No Class APIs,只用 Options APIs。

使用 Options APIs 最好是使用 export default Vue.extend({ ... }) 而不是 export default { ... }。

其实 Vue.js 3.0 早期是想要放弃 Class APIs 的,不过无奈想要兼容,所以才保留下来了。

相关扩展

插件的类型扩展,使用类型补充声明

import { AxiosInstance } from 'axios'

declare module 'vue/types/vue' {
  interface Vue {
    readonly $api: AxiosInstance
  }
}

JavaScript 项目中如何有更好的类型提示:JSDoc + import-types

https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html

https://www.typescriptlang.org/play/index.html?useJavaScript=truee=4#example/jsdoc-support

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342