帶你了解類型系統(tǒng)以及flow和typescript的基本使用

Study Notes

本博主會(huì)持續(xù)更新各種前端的技術(shù),如果各位道友喜歡,可以關(guān)注、收藏、點(diǎn)贊下本博主的文章。

類型系統(tǒng)

強(qiáng)類型語(yǔ)言

強(qiáng)類型指的是程序中表達(dá)的任何對(duì)象所從屬的類型都必須能在編譯時(shí)刻確定。

對(duì)于強(qiáng)類型語(yǔ)言,一個(gè)變量不經(jīng)過(guò)強(qiáng)制轉(zhuǎn)換,它永遠(yuǎn)是這個(gè)數(shù)據(jù)類型,不允許隱式的類型轉(zhuǎn)換。

假設(shè)定義了一個(gè) double 類型變量 a,不經(jīng)過(guò)強(qiáng)制類型轉(zhuǎn)換那么程序 int b = a 是無(wú)法通過(guò)編譯。

// 編譯失敗
double a;
int b = a;

強(qiáng)類型的優(yōu)點(diǎn)

  • 編譯時(shí)刻能檢查出錯(cuò)誤的類型匹配,以提高程序的安全性;
  • 可以根據(jù)對(duì)象類型優(yōu)化相應(yīng)運(yùn)算,以提高目標(biāo)代碼的質(zhì)量;
  • 重構(gòu)更牢靠;
  • 減少運(yùn)行時(shí)不必要的類型判斷。

弱類型語(yǔ)言

弱類型語(yǔ)言允許變量類型的隱式轉(zhuǎn)換,允許強(qiáng)制類型轉(zhuǎn)換等,如字符串和數(shù)值可以自動(dòng)轉(zhuǎn)化

let a = '100';
let b = 50;
console.log(a - b);
// 50 將a隱式轉(zhuǎn)換為Number
console.log(a + b);
// 10050 將b隱式轉(zhuǎn)換為String

靜態(tài)類型

靜態(tài)類型語(yǔ)言中,變量的類型必須先聲明,即在創(chuàng)建的那一刻就已經(jīng)確定好變量的類型,而后的使用中,你只能將這一指定類型的數(shù)據(jù)賦值給變量。如果強(qiáng)行將其他不相干類型的數(shù)據(jù)賦值給它,就會(huì)引發(fā)錯(cuò)誤。

動(dòng)態(tài)類型

動(dòng)態(tài)類型語(yǔ)言中,變量的類型可以隨時(shí)改變。

Flow

Flow 是 JavaScript 的靜態(tài)類型檢查器

安裝

安裝和配置項(xiàng)目的流程

安裝編譯器

首先,您需要配置一個(gè)編譯器以剝離 Flow 類型。您可以在 Babel 和 flow-remove-types 之間進(jìn)行選擇。

這邊以 Babel 為例:

Babel 是 JavaScript 代碼的編譯器,具有對(duì) Flow 的支持。Babel 可以將關(guān)于 Flow 代碼剔除。

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

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

接下來(lái),你需要在你的項(xiàng)目的根文件下創(chuàng)建一個(gè).babelrc。

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

剔除命令運(yùn)行

babel 輸入需剔除的文件或文件夾路徑 -d 輸出文件夾

配置流程

安裝 flow-bin

npm install --save-dev flow-bin

將"flow"腳本添加到您的 package.json:

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

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

npm run flow init

init 之后,運(yùn)行 flow

npm run flow

使用

Type Annotations(類型注解)

/**
 * Type Annotations(類型注解)
 * flow
 */
// 參數(shù)添加類型注解
function add(x: number, y: number) {
  return x + y;
}

// 正確
add(100, 100);
// 報(bào)錯(cuò)
// add('100', 100);

// 聲明基本類型數(shù)據(jù)時(shí)添加類型注解
let num: number = 100; // 正確
// num = '100'; // 報(bào)錯(cuò)

// 聲明函數(shù)時(shí)添加類型注解
function sum(): number {
  return 100; // 只能返回number類型數(shù)據(jù)
  // return '100'; // 報(bào)錯(cuò)
}

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!

將它們與聯(lián)合類型一起使用

/**
 * 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 將接受任何類型的值。字符串,數(shù)字,對(duì)象,函數(shù)等。

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

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

當(dāng)您嘗試使用 mixed 類型的值時(shí),必須首先弄清楚實(shí)際的類型是什么,否則最終會(huì)出錯(cuò)。

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

stringify('foo');

通過(guò) typeof 來(lái)確保該值是某種類型

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

stringify('foo');

Any Types(任何類型)

使用any是完全不安全的,應(yīng)盡可能避免使用。

/**
 * 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 類型用于這些值??赡茴愋涂梢耘c其他任何類型一起使用,只需在其前面加上一個(gè)問(wèn)號(hào)即可,例如?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(函數(shù)類型)

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(對(duì)象類型)

/**
 * Object Types(對(duì)象類型)
 * @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 標(biāo)準(zhǔn),可運(yùn)行在任何瀏覽器上。

TypeScript 是漸進(jìn)式的

目前官網(wǎng)上已更新到 TypeScript 4.0 ,而中文官網(wǎng)更新到 TypeScript 3.1

TypeScript(官網(wǎng))

TypeScript(中文網(wǎng))

安裝

這里是針對(duì)項(xiàng)目,不進(jìn)行全局安裝

npm i typescript -D

使用 ts-node 可以直接在 node 環(huán)境下運(yùn)行 ts 文件,方便開(kāi)發(fā)環(huán)境測(cè)試

npm i ts-node -D

運(yùn)行

ts-node 文件路徑

簡(jiǎn)單使用

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

test('typescript');

編譯 ts 代碼,生成一個(gè) index.js 文件,并被轉(zhuǎn)換為 es5

tsc index

index.js

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

配置

生成配置文件 tsconfig.json

tsc --init

具體配置可以查看Compiler Options(編譯選項(xiàng))

操作手冊(cè)

Basic Types(基礎(chǔ)類型)

為了使程序有用,我們需要能夠使用一些最簡(jiǎn)單的數(shù)據(jù)單元:數(shù)字,字符串,結(jié)構(gòu),布爾值等。 在 TypeScript 中,我們支持與 JavaScript 中期望的類型幾乎相同的類型,并拋出了方便的枚舉類型以幫助處理問(wèn)題。

Boolean(布爾類型)

let isDone: boolean = true;

Number(數(shù)字)

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(數(shù)組)

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 類型是對(duì) JavaScript 標(biāo)準(zhǔn)數(shù)據(jù)類型的一個(gè)補(bǔ)充。

像 C#等其它語(yǔ)言一樣,使用枚舉類型可以為一組數(shù)值賦予友好的名字。

enum Color {
  Red = 8,
  Green,
  Blue,
} // 默認(rèn)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

某種程度上來(lái)說(shuō),void 類型像是與 any 類型相反,它表示沒(méi)有任何類型。 當(dāng)一個(gè)函數(shù)沒(méi)有返回值時(shí),你通常會(huì)見(jiàn)到其返回值類型是 void:

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

聲明一個(gè) void 類型的變量沒(méi)有什么大用,因?yàn)槟阒荒転樗x予 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 類型是那些總是會(huì)拋出異?;蚋揪筒粫?huì)有返回值的函數(shù)表達(dá)式或箭頭函數(shù)表達(dá)式的返回值類型; 變量也可能是 never 類型,當(dāng)它們被永不為真的類型保護(hù)所約束時(shí)。

never 類型是任何類型的子類型,也可以賦值給任何類型;然而,沒(méi)有類型是 never 的子類型或可以賦值給 never 類型(除了 never 本身之外)。 即使 any 也不可以賦值給 never。

// 返回never的函數(shù)必須存在無(wú)法達(dá)到的終點(diǎn)
function error(message: string): never {
  throw new Error(message);
}

// 推斷的返回值類型為never
function fail() {
  return error('Something failed');
}

Object(對(duì)象類型)

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

函數(shù)

function add(x: number, y: number): number {
  return x + y;
}
const division = (x: number, y: number): number => {
  return x / y;
};
// 書寫完整函數(shù)類型
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會(huì)隱式類型推斷其為number
let name = '18'; // typescript會(huì)隱式類型推斷其為string
let className; // typescript會(huì)隱式類型推斷其為any

Type assertions(類型斷言)

有時(shí)候你會(huì)遇到這樣的情況,你會(huì)比 TypeScript 更了解某個(gè)值的詳細(xì)信息。 通常這會(huì)發(fā)生在你清楚地知道一個(gè)實(shí)體具有比它現(xiàn)有類型更確切的類型。

通過(guò)類型斷言這種方式可以告訴編譯器,“相信我,我知道自己在干什么”。 類型斷言好比其它語(yǔ)言里的類型轉(zhuǎn)換,但是不進(jìn)行特殊的數(shù)據(jù)檢查和解構(gòu)。 它沒(méi)有運(yùn)行時(shí)的影響,只是在編譯階段起作用。 TypeScript 會(huì)假設(shè)你,程序員,已經(jīng)進(jìn)行了必須的檢查。

類型斷言有兩種形式。 其一是“尖括號(hào)”語(yǔ)法:

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

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

另一個(gè)為 as 語(yǔ)法:

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 里,成員都默認(rèn)為 public

  • public

可以自由的訪問(wèn)程序里定義的成員

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

當(dāng)成員被標(biāo)記成 private 時(shí),它就只能在類的內(nèi)部訪問(wèn)。

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

protected 修飾符與 private 修飾符的行為很相似,但有一點(diǎn)不同, protected 成員在派生類中仍然可以訪問(wèn)。

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,不能被訪問(wèn)
  }
  getAge(): number {
    return this.age; // OK,可以被訪問(wèn)
  }
}

readonly(只讀)

你可以使用 readonly 關(guān)鍵字將屬性設(shè)置為只讀的。 只讀屬性必須在聲明時(shí)或構(gòu)造函數(shù)里被初始化

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'; // 錯(cuò)誤! name 是只讀的.

類實(shí)現(xiàn)接口

與 C#或 Java 里接口的基本作用一樣,TypeScript 也能夠用它來(lái)明確的強(qiáng)制一個(gè)類去符合某種契約。

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

抽象類

抽象類做為其它派生類的基類使用。 它們一般不會(huì)直接被實(shí)例化。 不同于接口,抽象類可以包含成員的實(shí)現(xiàn)細(xì)節(jié)。 abstract 關(guān)鍵字是用于定義抽象類和在抽象類內(nèi)部定義抽象方法。

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

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

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

Generics(泛型)

軟件工程中,我們不僅要?jiǎng)?chuàng)建一致的定義良好的 API,同時(shí)也要考慮可重用性。 組件不僅能夠支持當(dāng)前的數(shù)據(jù)類型,同時(shí)也能支持未來(lái)的數(shù)據(jù)類型,這在創(chuàng)建大型系統(tǒng)時(shí)為你提供了十分靈活的功能。

在像 C#和 Java 這樣的語(yǔ)言中,可以使用泛型來(lái)創(chuàng)建可重用的組件,一個(gè)組件可以支持多種類型的數(shù)據(jù)。 這樣用戶就可以以自己的數(shù)據(jù)類型來(lái)使用組件。

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

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

// 箭頭函數(shù)
const createArrayArrow = <T>(...args: T[]): T[] => {
  return args;
};

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

更多

定義組件的幾種不同方式

使用 Options APIs

  • 組件仍然可以使用以前的方式定義(導(dǎo)出組件選項(xiàng)對(duì)象,或者使用 Vue.extend())
  • 但是當(dāng)我們導(dǎo)出的是一個(gè)普通的對(duì)象,此時(shí) TypeScript 無(wú)法推斷出對(duì)應(yīng)的類型,
  • 至于 VSCode 可以推斷出類型成員的原因是因?yàn)槲覀兪褂昧?Vue 插件,
  • 這個(gè)插件明確知道我們這里導(dǎo)出的是一個(gè) Vue 對(duì)象。
  • 所以我們必須使用 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 的組件可以使用一個(gè)繼承自 Vue 類型的子類表示,這種類型需要使用 Component 裝飾器去修飾

裝飾器是 ES 草案中的一個(gè)新特性,提供一種更好的面向切面編程的體驗(yàn),不過(guò)這個(gè)草案最近有可能發(fā)生重大調(diào)整,所以個(gè)人并不推薦。

裝飾器函數(shù)接收的參數(shù)就是以前的組件選項(xiàng)對(duì)象(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: 使用類的實(shí)例屬性聲明
  • Method: 使用類的實(shí)例方法聲明
  • Computed: 使用 Getter 屬性聲明
  • 生命周期: 使用類的實(shí)例方法聲明

其它特性:例如 components, props, filters, directives 之類的,則需要使用修飾器參數(shù)傳入

使用這種 class 風(fēng)格的組件聲明方式并沒(méi)有什么特別的好處,只是為了提供給開(kāi)發(fā)者多種編碼風(fēng)格的選擇性

使用 Class APIs + vue-property-decorator

這種方式繼續(xù)放大了 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')
  }
}

個(gè)人最佳實(shí)踐

No Class APIs,只用 Options APIs。

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

其實(shí) Vue.js 3.0 早期是想要放棄 Class APIs 的,不過(guò)無(wú)奈想要兼容,所以才保留下來(lái)了。

相關(guān)擴(kuò)展

插件的類型擴(kuò)展,使用類型補(bǔ)充聲明

import { AxiosInstance } from 'axios'

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

JavaScript 項(xiàng)目中如何有更好的類型提示: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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容