型別系統

2020-08-09 02:58:28

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