TypeScript 學習筆記 — 型別相容 (十)

2023-03-20 18:07:51


TS 是結構型別系統(structural type system),基於結構/形狀檢查型別,而非型別的名字。

TS 中的相容性,主要看結構是否相容。(核心是考慮安全性),結構化的型別系統(又稱鴨子型別檢查),如兩個型別名字不一樣但是無法區分
型別相容性是基於結構子型別的。 結構型別是一種只使用其成員來描述型別的方式。

如果x要相容y,那麼y至少具有與x相同的屬性。
這裡要檢查y是否能賦值給x,編譯器檢查x中的每個屬性,看是否能在y中也找到對應屬性。
X 相容 Y:X(目標型別)= Y(源型別)
簡單一句話概括相容性: 重新賦值不報錯(型別自動轉化)

一.基本資料型別的相容性

let temp: string | number;
let num!: number;
temp = num;
let obj: {
  toString(): string;
};
let str: string = "yya";
obj = str; // 字串中具備toString()方法,所以可以進行相容
obj.toString(); // 安全, 保證使用的時候不會發生異常

二.介面相容性

介面的相容性,只要滿足介面中所需要的型別即可!(保證你要的,我都有,就行,多了也沒關係)

interface IAnimal {
  name: string;
  age: number;
}
interface IPerson {
  name: string;
  age: number;
  address: string;
}
let animal: IAnimal;
let person: IPerson = {
  name: "yya",
  age: 18,
  address: "beijing",
};

type T2 = IPerson extends IAnimal ? true : false; // true
animal = person; // 子類賦予給父類別 相容

三.函數的相容性

函數的相容性主要是比較引數和返回值

引數:賦值函數的引數要少於等於被賦值的函數:也就是說,對應函數的引數來講,少的引數可以賦予給多的,因為內部實現傳了多個可以少用或不用(忽略額外的引數在 JavaScript 裡是很常見的)

sum2的每個引數必須能在sum1裡找到對應型別的引數。 注意的是引數的名字相同與否無所謂,只看它們的型別sum2的每個引數在sum1中都能找到對應的引數,所以允許賦值。

let sum1 = (a: string, b: string) => a + b;
let sum2 = (a: string) => a;
sum1 = sum2;

舉例: Array#forEach給回撥函數傳 3 個引數:item,index 和 array。 儘管如此,傳入一個只使用第一個引數的回撥函數也是可以的

type Func<T> = (item: T, index: number, array: any[]) => void;
function forEach<T>(arr: T[], cb: Func<T>) {
  for (let i = 0; i < arr.length; i++) {
    cb(arr[i], i, arr);
  }
}
forEach([1, 2, 3], (item) => {
  console.log(item);
});

返回值:

type sum1 = () => string | number;
type sum2 = () => string;

let fn1: sum1;
let fn2!: sum2;
fn1 = fn2;

四.類的相容性

類與物件字面量和介面差不多,但有一點不同:類有靜態部分和範例部分的型別。 比較兩個類型別的物件時,只有範例的成員會被比較。 靜態成員和建構函式不在比較的範圍內。

class Animal {
  feet!: number;
  constructor(name: string, numFeet: number) {}
}

class Size {
  feet!: number;
  constructor(numFeet: number) {}
}

let a!: Animal;
let s!: Size;

a = s; // OK
s = a; // OK

類的私有成員和受保護成員

只要有 private 或者 protected 關鍵字會影響相容性, 當檢查類範例的相容時,如果目標型別包含一個 private 私有成員,那麼源型別必須包含來自同一個類的這個私有成員。 這條規則也適用於包含 protected 受保護成員範例的型別檢查。 允許子類賦值給父類別,但是不能賦值給其它有同樣型別的類。

class A {
  private name!: string;
  age!: number;
}

class B {
  private name!: string;
  age!: number;
}

// let a: A = new B();  // error

class Parent {
  protected name: string = "zf";
  age: number = 11;
}
class Child extends Parent {}
let child: Parent = new Child(); // ok

五.泛型的相容性

泛型比較的是最終的結果 比較的不是泛型傳遞的引數
例一:

interface Empty<T> {}

let x: Empty<number>;
let y!: Empty<string>;

type xx = Empty<number> extends Empty<string> ? true : false; // true

x = y; // OK 因為 y 匹配 x 的結構

在例一中,x 和 y 是相容的,因為它們的結構使用型別引數時並沒有什麼不同。 把這個例子改變一下,增加一個成員,就能看出是如何工作的了:
例二:

interface NotEmpty<T> {
  data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;

type xx = NotEmpty<number> extends NotEmpty<string> ? true : false; // false

x = y; // Error,  不相容

對於沒指定泛型型別的泛型引數時,會把所有泛型引數當成 any 比較。 然後用結果型別進行比較,就像例一:

let identity = function <T>(x: T): T {};

let reverse = function <U>(y: U): U {};

identity = reverse; // OK,  (x: any) => any 匹配 (y: any) => any

六.列舉的相容性

列舉型別與數位型別相容,並且數位型別與列舉型別相容

enum Status {
  Pending,
  Resolved,
  Rejected,
}

let current = Status.Pending;
let num = 0;

current = num;
num = current;

不同列舉型別之間是不相容的。

enum Status {
  Pending,
  Resolved,
  Rejected,
}
enum Color {
  Red,
  Blue,
  Green,
}

let current = Status.Pending;
let color = Color.Red;

current = color; // 不能將型別「Color.Red」分配給型別「Status」

標稱型別簡短介紹

型別分為兩種 結構化型別(structural type system) 、標稱型別(nominal type system)
標稱型別: 雖然 BTC,USDT 都是 number 型別,但還是想要用不同的型別表示,且不能互換,資料的值本身沒什麼區別,安上不同名字就是不同型別,也就是說,標稱型別系統中,兩個變數是否型別相容(可以交換賦值)取決於這兩個變數顯式宣告的型別名字是否相同。

class AddType<S> {
  private _type!: S;
}
type NewType<T, S extends string> = T & AddType<S>;

type BTC = NewType<number, "btc">; // number + BTC
type USDT = NewType<number, "usdt">; // number + USDT
let btc = 100 as BTC;
let usdt = 100 as USDT;

function getCount(count: USDT) {
  return count;
}
getCount(usdt);