泛型 -- Typescript基礎篇(11)

咱們在定義類型時,除了要給類型具體的規範和約束外,另外一個重要考量是可否方便的複用。typescript

爲何要有泛型

假如咱們有一個foo的方法,它的參數是多個字符串,用於將全部字符串參數拼起來:函數

function foo(...list: string[]): string {
  return list.join(",");
}

foo("str1", "str2", "str3", "str4");
// result: str1,str2,str3,str4
複製代碼

如今咱們有一個新的需求,傳入的參數改成多個數值, 須要把全部的數值拼起來,那咱們又得定義一個bar方法:ui

function bar(...list: number[]): string {
   return list.join(",");
}

bar(1, 2, 3, 4)
// result: 1,2,3,4
複製代碼

能夠看出這兩個方法實現代碼實際上是如出一轍的,惟一的區別是參數和返回值的類型不一樣。固然,爲了消除這種重複,咱們能夠把參數和返回值類型改成any,但這樣會失去類型保護;或者咱們也能夠用聯合類型,結合函數重載實現,但也比較繁瑣,尤爲是可選類型數量不少後。this

而經過使用泛型就能很好的解決這個問題,達到類型保護和重用的目的:spa

function foo<T>(...list: T[]): string {
  return list.join(",");
}

// 只能輸入string類型變量
foo<string>("str1", "str2", "str3", "str4");

// 只能輸入number類型變量
foo<number>(1, 2, 3, 4)
複製代碼

咱們使用T表明類型(泛型名字能夠是其餘任意字符串),在實際調用函數時再傳入參數的類型做爲約束。泛型能夠應用於函數,接口,類型別名,類等常見類型。code

就算在調用foo方法時,不顯式指定類型,ts也能根據實參推導出T的具體類型:cdn

generics-hinter

多個參數

泛型的參數能夠是多個,各個參數使用,隔開:對象

function merge<T, U>(a: T, b: U): T & U {
  return Object.assign(a, b);
}

const result = merge({ name: "xxx" }, { age: 12 });
// result = { name: 'xxx', age: 12 }
複製代碼

指定泛型類型時,要麼所有指定,要麼都不指定(依賴於ts自動推斷)。不能只指定一部分(除非剩下的部分都有默認值):blog

function fn<T, U>(a: T, b: U) {}

// 合法
fn<string,number>("1", 1);
fn("1", 1);

// 不合法
fn<string>("1", 1);
複製代碼

泛型約束

在函數內部使用泛型變量時,因爲不能事先肯定它是哪一種類型,因此沒法隨意調用屬於它的屬性和方法:繼承

function countLength<T>(a: T) {
	// 沒法獲取a.length:Property 'length' does not exist on type 'T'.
  console.log("object's length is" + a.length);
}
複製代碼

這時咱們能夠對T進行約束,T必須是一個有length屬性的對象:

interface Lengthy {
  length: number;
}

function countLength<T extends Lengthy>(a: T) {
  console.log("object's length is" + a.length);
}
複製代碼

咱們首先定義了一個Lengthy的接口,它具備length屬性。而且讓T繼承Lengthy,保證了T是一個具備length屬性的對象,實際使用時:

// 合法
countLength({ length: 10, name: "baba" });

// 不合法
// Property 'length' is missing in type '{}' but required in type 'Lengthy'
countLength({});
複製代碼

咱們重構一下merge的方法。在merge方法中,咱們雖然指望傳入的是兩個對象,但實際並無在約束,因此實際上任何類型都是合法的。雖然Object.assign對不一樣的類型的變量作了必定的額外處理保證不報錯,可是實際結果可能會和預想結果不一樣。咱們爲泛型加上約束,保證傳入的參數必須是兩個對象:

function merge<T extends object, U extends object>(a: T, b: U): T & U {
  return Object.assign(a, b);
}
複製代碼

多個類型之間也能夠相互約束:

function copyProperties<T extends U, U extends object>(target: T, source: U) {
  for (const key in source) {
    (target as U)[key] = source[key];
  }
  return target;
}

// 合法
copyProperties({ name: "xxx", age: 20 }, { age: 30 });

// 不合法,由於target不兼容source
copyProperties({ name: "xxx" }, { age: 30 });
複製代碼

T繼承U,保證了T包含U上的全部屬性。

舉個更復雜的例子。咱們有一個方法getProperty,它有兩個參數,objkey。它的做用是獲取obj[key]的值:

function getProperty<T extends object>(obj: T, key: string) {
  return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // 1
getProperty(x, "m"); // undefined
複製代碼

實際上咱們並無對key進行約束,咱們指望的key只能是obj中的屬性。此時可使用keyof關鍵字:

interface Obj {
  name: string;
  age: number;
}

type Key = keyof Obj; // Key = "name" | "age"
複製代碼

通過約束後的方法爲:

function getProperty<T extends object, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // 1
getProperty(x, "m"); // Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'
複製代碼

默認值

咱們也能夠爲泛型添加默認類型,這個默認類型會在沒有指定類型參數,或者是ts沒法從實際參數中推斷出類型時生效。

function createArray<T = string>(length: number, value: T): T[] {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}
複製代碼

咱們使用T = string給了T一個默認的類型。注意:

  • 若是一個泛型有默認值,那麼該泛型就是可選泛型
  • 可選泛型聲明必須在沒有默認值的泛型以後
  • 若是泛型有約束,那麼默認值必須兼容這個約束
// 可選泛型排在後面
function fn<T, U = string>(a: T, b: U) {}

interface Lengthy {
  length: number;
}

// 默認值要兼容Lengthy接口
function countLength<T extends Lengthy = string[]>(a: T) {
  console.log("object's length is" + a.length);
}
複製代碼

泛型接口

泛型應用於接口:

interface Model<T> {
  data: T;
  name: string;
}

const model: Model<number> = {
  name: "model",
  data: 1,
};
複製代碼

接口也能夠表示泛型函數類型:

interface LogArrayFn<T> {
  (array: T[]): void;
}

// 實際聲明函數時,須要指定泛型
const fn: LogArrayFn<string> = (array) => {
  console.log(array);
};

fn(["12"]);

// 另外一個形式
interface LogArrayFn {
  <T>(array: T[]): void;
}

const fn: LogArrayFn = (array) => {
  console.log(array);
};

// 在調用函數時指定泛型,或者依賴ts的自動推斷
fn<string>(["12"]);
複製代碼

泛型別名

類型別名也可使用泛型,其使用方法和接口十分相似:

type AliasModel<T> = {
  data: T;
  name: string;
};

const t: AliasModel<number> = {
  name: "xxx",
  data: 1,
};
複製代碼

使用別名聲明泛型函數:

// 聲明以及使用和接口類型
type AliasLogArrayFn<T> = {
  (array: T[]): void;
};

// 或者
type AliasLogArrayFn = {
    <T>(array: T[]): void;
}; 


// 簡化版
type AliasLogArrayFn<T> = (array: T[]) => void;

type AliasLogArrayFn = <T>(array: T[]) => void;

複製代碼

泛型類

與泛型接口相似,泛型也能夠用於類的類型定義中:

class GenericData<T> {
  data: T;
  constructor(data: T) {
    this.data = data;
  }

  logData() {
    console.log(this.data);
  }
}

// 推斷出T 爲 { name: string }
const data = new GenericData({ name: "xxx" });
複製代碼

若是類只包含一個或多個泛型函數,也能夠是:

class GenericData {
  logData<T>(data: T) {
    console.log(data);
  }
}
複製代碼

泛型綁定具體類型時機

對於不一樣的類型,泛型綁定具體類型的時機(或者是根據參數推斷具體類型的時機)是不一樣的:

  • 對於泛型函數:當調用函數時綁定
  • 對於泛型類:當實例化時綁定
  • 對於泛型接口或泛型類型別名:當使用它們時綁定

這能夠解釋如下現象:

// 第一種
type AliasLogArrayFn<T> = (array: T[]) => void;
const fn: AliasLogArrayFn<number> = (array) => console.log(array);
fn([1,2])

// 第二種
type AliasLogArrayFn = <T>(array: T[]) => void;
const fn: AliasLogArrayFn = (array) => console.log(array);
fn<number>([1, 2, 3]);
複製代碼

第一種聲明方式獲得的是一個泛型別名,在使用泛型別名時就須要綁定類型,因此聲明fn時須要顯式指定類型,至關於聲明瞭具體類型的方法。

第二種獲得的是表明泛型函數的別名(自己不是泛型別名),因此聲明fn時至關於在聲明一個泛型方法,不須要綁定類型,而在調用時須要綁定類型。

相關文章
相關標籤/搜索