[TypeScript] 泛型

有時一段程式碼可能會根據它的呼叫方式來處理各種不同的型別。

以下這個例子,identity 函式,含有接收任何可能型別的輸入,並回傳相同的輸入作為輸出:

function identity(input){
  return input;
}

identity("abc");
identity(123);
identity({ quote: "aaa" });

可將 input 宣告為 any,而函式回傳型別也可以是 any:

function identity(input: any){
  return input;
}

let value = identity(123); // value 的型別為 any

有鑑於 input 是允許任何輸入,需要一種方式來說明 input 型別與函式回傳的型別,兩者之間存在的關係。TypeScript 使用泛型(generics)來抓住型別之間的關係。

通常用單字母名稱來表示,例 T、U 或駝峰式命名,然後使用左右角括號來宣告,例:someFunction<T>

泛型函式

可透過將參數型別的別名放在角括號中,角括號放在參數的小括號之前,就可以將該函式成為泛型。然後這個別名,可用於註記參數型別、回傳型別和函式本體的型別註記。例:

function identity<T>(input: T) {
  return input;
}

const numeric = identity("hi"); // 型別:"hi"
const stringy = identity(123);  // 型別:123

也可用在箭頭函式:

const identity = <T>(input: T) => input;

identity(123); // 型別:123

替函式加上參數型別,允許函式被不同的輸入重複使用,同時仍然保持型別安全,並避免 any 型別。

明確的泛型呼叫型別

TypeScript 會將無法推斷的引數型別,預設為 unknown 型別。例:

function logWrapper<Input>(callback: (input: Input) => void){
  return (input: Input) => {
    console.log("Input:", input);
    callback(input);
  };
}

// 型別:(input: string) => void
logWrapper((input: string) => {
  console.log(input.length);
});

// 型別:(input: unknown) => void
logWrapper((input) => {
  // 報錯:unknown 型別不存在 length 屬性
  console.log(input.length);
});

為了避免預設為 unknown,可使用明確的泛型引數來呼叫函式,該引數型別明確告訴 TypeScript 引數應該是什麼型別。例:

// 型別:(input: string) => void
logWrapper<string>((input) => {
  console.log(input.length);
});

多個函式參數型別

以下的 makeTuple 宣告了兩個參數型別,並回傳一個唯讀元組型別的資料,例:

function makeTuple<First, Second>(first: First, second: Second){
  return [first, second] as const;
}

// 以下 tuple 的型別:唯讀 [boolean, string]
let tuple = makeTuple(true, "abc");

留意:如果一個函式宣告多個參數型別,則對該函式的呼叫,必須明確宣告任何所有泛型型別。例:

function makePair<Key, Value>(key: Key, value: Value){
  return {key, value};
}

// 正確,沒有提供引數型別,其型別為:{key: string; value: number}
makePair("abc", 123);

// 正確,型別為:{key: string; value: number}
makePair<string, number>("abc", 123);
// 正確,型別為:{key: "abc"; value: 123}
makePair<"abc", 123>("abc", 123);

// 報錯:有2個引數型別,但只設定1個
makePair<string>("abc", 123);

泛型介面

介面也可以宣告為泛型。

以下例子中的 Box 宣告,有一個屬性的 T 參數型別。建立一個宣告為帶有型別 Box 引數的物件,並且需強制符合 inside: T 屬性引數型別:

interface Box<T> {
  inside: T;
}

let stringyBox: Box<string> = {
  inside: abc"
};

let numberBox: Box<number> = {
  inside: 123
};

let incorrectBox: Box<number> = {
  inside: false
  // 報錯:型別 boolean 不可指派給型別 number
};

推斷泛型介面的型別

interface LinkedNode<Value> {
  next?: LinkedNode<Value>;
  value: Value;
}

function getLast<Value>(node: LinkedNode<Value>): Value {
  return node.next ? getLast(node.next) : node.value;
}

// 推斷 value 引數型別:Date
let lastDate = getLast({
  value: new Date("09-13-1993")
});

// 推斷 value 引數型別:string
let lastFruit = getLast({
  next: {
    value: "banana"
  },
  value: "apple"
});

// 推斷 value 引數型別:number
let lastMismatch = getLast({
  next: {
    value: 123
  },
  // 錯誤:型別 boolean 不可指派給型別 number
  value: false
});

泛型類別

可宣告任意數量的參數型別,提供給成員使用。例:

// 宣告了 Key 和 Value 兩個參數型別
class Secret<Key, Value> {
  key: Key;
  value: Value;

  constructor(key: Key, value: Value) {
    this.key = key;
    this.value = value;
  }

  getValue(key: Key): Value | undefined {
    return this.key === key ? this.value : undefined;
  }
}

// 型別:Secret<number, string>
const storage = new Secret(123, "abc");
// 型別:string | undefined
storage.getValue(1987);

明確的泛型類別型別

例如上述例子的:

new Secret(123, "abc");

TypeScript 可以推斷出它的型別,然而,如果無法從傳遞給其建構函式的參數推斷出引數型別的話,預設為 unknown。

例:

class CurriedCallback<Input> {
  #callback: (input: Input) => void;

  constructor(callback: (input: Input) => void) {
    this.#callback = (input: Input) => {
      console.log("Input:", input);
      callback(input);
    };
  }

  call(input: Input) {
    this.#callback(input);
  }
}

// 型別:CurriedCallback<string>
new CurriedCallback((input: string) => {
  console.log(input.length);
});

// 型別:CurriedCallback<unknown>
new CurriedCallback((input) => {
  // 報錯:unknown 型別不存在 length 屬性
  console.log(input.length);
});

可透過明確的引數型別來避免預設為 uknown,例:

// 型別:CurriedCallback<string>
new CurriedCallback<string>((input) => {
  console.log(input.length);
});

繼承(擴充)泛型類別

例:

class Quote<T> {
  lines: T;

  constructor(lines: T) {
    this.lines = lines;
  }
}

// SpokenQuote 類別替它的基本類別 Quote<T> 提供 string 陣列作為 T 的引數型別
class SpokenQuote extends Quote<string[]> {
  speak() {
    console.log(this.lines.join("\n"));
  }
}

new Quote("abc").lines; // 型別:string
new Quote([1, 2, 3]).lines; // 型別:number[]

new SpokenQuote(["a", "b"]).lines; // 型別:string[]

// 報錯:型別 number 不可指派給型別 string
new SpokenQuote([1, 2, 3]);

實現泛型介面

例:

// 這個是泛型介面
interface ActingCredit<Role> {
  role: Role;
}

class MoviePart implements ActingCredit<string> {
  role: string;
  speaking: boolean;

  constructor(role: string, speaking: boolean) {
    this.role = role;
    this.speaking = speaking;
  }
}

const part = new MoviePart("aaa", true);

part.role; // 型別:string

class IncorrectExtension implements ActingCredit<string> {
  role: boolean;
  // 以上這行會報錯:型別 boolean 不可指派給型別 string
}

泛型方法

例:

// 類別中宣告了一個泛型型別 Key
class CreatePairFactory<Key> {
  key: Key;

  constructor(key: Key) {
    this.key = key;
  }

  createPair<Value>(value: Value) {
    return {key: this.key, value};
  }
}

// 型別:CreatePairFactory<string>
const factory = new CreatePairFactory("role");

// 型別:{key: string, value: number}
const numberPair = factory.createPair(10);

// 型別:{key: string, value: string}
const stringPair = factory.createPair("bbb");

靜態泛型型別

類別當中的靜態成員無權存取任何類別實體的型別資訊。例:

class BothLogger<OnInstance> {
  instanceLog(value: OnInstance) {
    console.log(value);
    return value;
  }
  // 靜態 staticLog 方法
  static staticLog<OnStatic>(value: OnStatic) {
    // 報錯:靜態成員不得參考類別引數型別
    let fromInstance: OnInstance;
    console.log(value);
    return value;
  }
}

const logger = new BothLogger<number[]>;
logger.instanceLog([1, 2, 3]); // 型別:number[]

// 推斷 OnStatic 引數型別:boolean[]
BothLogger.staticLog([false, true]);

// 明確顯示 OnStatic 引數型別:string
BothLogger.staticLog<string>("hi");

泛型型別別名

TypeScript 當中,每個型別別名可以被給予任意數量的參數型別,例如以下的 Nullish 型別接收一個泛型 T:

type Nullish<T> = T | null | undefined;

泛型型別別名通常與函式搭配使用,用來描述泛型函式的型別:

type CreatesValue<Input, Output> = (input: Input) => Output;

// 型別:(input: string) => number
let creator: CreatesValue<string, number>;

creator = text => text.length; // 正確

// 報錯:型別 string 不可指派給型別 number
creator = text => text.toUpperCase();

泛型可辨識的聯集

例:

type Result<Data> = FailureResult | SuccessfulResult<Data>;

interface FailureResult {
  error: Error;
  succeeded: false;
}

interface SuccessfulResult<Data> {
  data: Data;
  succeeded: true;
}

function handleResult(result: Result<string>) {
  if(result.succeeded){
    // 進到這裡,result 的型別: SuccessfulResult<string>
    console.log("hihi" + result.data);
  }else{
    // 進到這裡,result 的型別: FailureResult
    console.error("err" + result.error);
  }

  // 報錯:型別 Result<string> 沒有屬性 data。
  // 型別 FailureResult 沒有屬性 data。
  result.data;
}

泛型修飾符號

泛型預設值

給泛型一個預設值。例:

// Quote 接受一個 T 型別的參數,其型別預設為 string
interface Quote<T = string> {
  value: T;
}

// explicit 變數將 T 明確設定為 number
let explicit: Quote<number> = {
  value: 123
};

// implicit 變數,其 T 型別預設為 string
let implicit: Quote = {
  value: "aaa"
};


let mismatch: Quote = {
  // 報錯:型別 number 不可指派給型別 string
  value: 123
};

需留意的是,所有預設參數型別必須放在宣告清單中的最後位置。

受限制的泛型型別

限制參數型別的語法,是將 extends 關鍵字放在參數型別的名稱之後,然後是限制它的型別。

例:

interface WithLength {
  length: number;
}

function logWithLength<T extends WithLength>(input: T){
  console.log("Length: " + input.length);
  return input;
}

logWithLength("abc"); // 型別:string
logWithLength([false, true]); // 型別:boolean[]
logWithLength({ length: 123 }) // 型別:{length: number}

logWithLength(new Date()); // 報錯:缺少 length 屬性

keyof 和限制參數型別

例:

function get<T>(container: T, key: keyof T) {
  return container[key];
}

const roles = {
  favofite: "Fargo",
  others: ["a", "b", "c"]
};

const found = get(roles, "favorite"); // 型別:string | string[]

Promise

留言

發佈留言