TypeScript 中的工具類型

Shavahn is a dickhead.png

我們知道在 TypeScript 中一切皆是類型,而 TypeScript 除了能夠創(chuàng)建新類型之外,它還提供了一些工具類型實(shí)現(xiàn)轉(zhuǎn)換現(xiàn)有類型的能力,這些工具類型是 TypeScript 內(nèi)置的,同時(shí)它們?nèi)挚捎谩?/p>

Partial<Type>

Partial 會(huì)創(chuàng)建一個(gè)新的類型同時(shí)它內(nèi)部所有屬性都變成可選的。

type Type = { x: string, y: string };

// { x?: string; y?: string }
type PartialType = Partial<Type>;

Partial 最常使用的地方是讓一個(gè)對(duì)象的所有屬性都變的可選,比如下面這個(gè)例子,我們?nèi)ジ掠脩舻哪承┬畔ⅲ?/p>

interface User {
  name: string;
  surname: string;
  age: number;
}

const updateUser = (user: User, fields: Partial<User>): User => ({
  ...user,
  ...fields,
});

const user1: User = {
  name: "John",
  surname: "Doe",
  age: 17,
};

const user2 = updateUser(user1, { age: 18 });

// { name: "John", surname: "Doe", age: 18 }
console.log(user2);

我們看到 Partial 工具類型非常的好用,簡單方便,我們很有必要研究下它的源碼,這樣當(dāng)官方提供的工具類型不滿足我們的需求的時(shí)候,我們還可以定制化,雖然很少遇到。

Partial 工具類型源碼:

type Partial<T> = {
    [P in keyof T]?: T[P];
};

我們看到,Partial 本質(zhì)是一個(gè)——泛型類型別名,里面還用到了索引類型映射類型。

如果你很熟悉 TS 語法,完整的 Partial 寫法應(yīng)該是這樣的:

type Partial<T> = {
    [P in keyof T]+?: T[P]; // 多個(gè)加號(hào)
};

Required<Type>

Required 是 Partial 的反面,Required 創(chuàng)造一個(gè)新類型,同時(shí)內(nèi)部所有的屬性都是必須的。

type Type = { x?: string, y?: string };

// { x: string; y: string }
type RequiredType = Required<Type>;

使用事例例如:

interface User {
  name?: string;
  surname?: string;
  age?: number;
}

class UserManager {
  private user: Required<User>;

  constructor(user: User) {
    this.user = {
      name: user.name || "Not Set",
      surname: user.surname || "Not Set",
      age: user.age || 0,
    };
  }

  getUser() {
    return this.user;
  }
}

Partial 工具類型的完整源碼我們已經(jīng)知道了,現(xiàn)在 Required 的源碼相信你自己也能實(shí)現(xiàn),很簡單,我們只需要改下類型別名同時(shí)把加號(hào)換成減號(hào)就行了:

type Required<T> = {
    [P in keyof T]-?: T[P];
};

Readonly<Type>

Readonly<T> 創(chuàng)建一個(gè)新類型,同時(shí)所有屬性都變?yōu)橹蛔x屬性,這也就意味著這些屬性不能被重新賦值。

type Type = { x: string, y: string };

// { readonly x: string; readonly y: string }
type ReadonlyType = Readonly<Type>;

當(dāng)我們使用 Object.freeze 的時(shí)候,Readonly 是非常的好用的:

interface User {
  name: string;
  surname: string;
  age: number;
}

const user: User = {
  name: "John",
  surname: "Doe",
  age: 18,
};

function freeze<T>(obj: T): Readonly<T> {
  return Object.freeze(obj);
}

const readonlyUser = freeze(user);

// Cannot assign to "name" because it is a read-only property
readonlyUser.name = "Andrew";

這時(shí),我們會(huì)很自然的寫出 Readonly 的源碼,如下:

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

上面這個(gè)代碼并不完整,我簡單提醒下你,你就能想出來它的完整代碼了,想一下如何去除有 readonly 屬性的工具函數(shù)。完整版 Readonly 是不是已經(jīng)出現(xiàn)在你的腦海里了,沒錯(cuò)只是簡單的在 readonly 前面加個(gè)加號(hào):

type Readonly<T> = {
    +readonly [P in keyof T]: T[P];
};

TypeScript 沒有給出去除 Readonly 修飾符工具函數(shù)此時(shí)我們就可以自己來實(shí)現(xiàn)了,我們把這個(gè)工具類型叫 NonReadonly。

type position = { readonly x: string; readonly y: string};

type NonReadonly<T> = {
    -readonly [P in keyof T]: T[P]
}

// type NonReadonlyPos = { x: string; y: string; }
type NonReadonlyPos = NonReadonly<position>;

Record<Keys, Type>

Record<Keys, Type> 創(chuàng)造一個(gè)新類型,同時(shí)將 Keys 中所有的屬性的值的類型轉(zhuǎn)化為 T 類型。

// { x: string; y: string }
type Type = Record<"x" | "y", string>;

Record 可以常用來組合:


interface UserInfo {
  age: number;
}

type UserName = "john" | "andrew" | "elon" | "jack";

const userList: Record<UserName, UserInfo> = {
  john: { age: 18 },
  andrew: { age: 20 },
  elon: { age: 49 },
  jack: { age: 56 },
};

源碼實(shí)現(xiàn):

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

泛型約束 K extends 相信你能看懂,keyof any 你可能有點(diǎn)犯迷糊,keyof any 表示對(duì)象 key 的類型,所以 keyof any === string | number | symbol,不信你可以復(fù)制以下代碼,在 TS 環(huán)境測(cè)試下:

// type unionKeyType = string | number | symbol
type unionKeyType = keyof any;

Exclude<Type, ExcludedUnion>

Exclude 通過排除類型中可分配給 ExcludedUnion 的所有聯(lián)合成員來創(chuàng)建新類型:

// "x" | "y"
type ExcludedType = Exclude<"x" | "y" | "z", "z">;

確定從對(duì)象中獲取固定的 key 非常有用:

interface User {
  name: string;
  surname: string;
  personalNumber: number;
}

type AllowedKeys = Exclude<keyof User, "personalNumber">;

const getUserProperty = (user: User, key: AllowedKeys) => user[key];

const user: User = {
  name: "John",
  surname: "Doe",
  personalNumber: 999999999,
};

const nameProp = getUserProperty(user, "name");
const surnameProp = getUserProperty(user, "surname");

// Argument of type "personalNumber" is not assignable to parameter of type "name" | "surname"
const personalNumberProp = getUserProperty(user, "personalNumber");

源碼展現(xiàn),就一個(gè)簡單的條件類型。

type Exclude<T, U> = T extends U ? never : T;

Extract<Type, Union>

Extract 是 Exclude 的反面。

它通過從可分配給聯(lián)合的類型中提取所有聯(lián)合成員來創(chuàng)建新類型。

// "x" | "y"
type ExtractedType = Extract<"x" | "y" | "z", "x" | "y">;

用來提取兩個(gè)類型的公有屬性名會(huì)非常的合適:

interface Human {
  id: string;
  name: string;
  surname: string;
}

interface Cat {
  id: string;
  name: string;
  sound: string;
}

// "id" | "name"
type CommonKeys = Extract<keyof Human, keyof Cat>;

源碼展示:

type Extract<T, U> = T extends U ? T : never;

Pick<Type, Keys>

Pick 的作用是將 Type 類型中的 Keys 類型提取出來,創(chuàng)建為一個(gè)新類型。

type LongType = {
  a: string;
  b: string;
  c: string;
  d: string;
};

// { a: string; b: string }
type ShortType = Pick<LongType, "a" | "b">;

Pick 創(chuàng)建的類型是 Type 類型的子類型,所以它的使用常常是從一個(gè)大類型中提取某些小類型。

interface User {
  name: string;
  surname: string;
  street: string;
  house: number;
}

type UserAddress = Pick<User, "street" | "house">;

const address: UserAddress = {
  street: "Street",
  house: 1,
};

源碼實(shí)現(xiàn),注意下泛型約束 K extends keyof T。

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

Omit<Type, Keys>

Omit 從 Type 的所有屬性中,移除 Keys 鍵用剩下的鍵來創(chuàng)建新類型。

type LongType = {
  a: string;
  b: string;
  c: string;
  d: string;
};

// { c: string; d: string }
type ShortType = Omit<LongType, "a" | "b">;

這個(gè)用來刪除類型中的某些不要的屬性非常有用:

interface User {
  name: string;
  surname: string;
  personalNumber: number;
}

type CleanUser = Omit<User, "personalNumber">;

const getUserData = (user: User): CleanUser => {
  const { personalNumber, ...rest } = user;
  return rest;
};

源碼展示, Pick 的實(shí)現(xiàn)用到了 Exclude 來實(shí)現(xiàn)的:

type Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P]; }

如果你用較早期的 TS ,Omit 的實(shí)現(xiàn)可能是這樣的,效果一樣,思路不通而已:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; 

keyof any 等價(jià)于 number | string | symbol,我們知道 number | string | symbol 是能作為對(duì)象的 key 的類型,也就是說 Omit<Type, Keys> 移除鍵的時(shí)候,keys 可以不為 Type 里面的 key ,簡單來講寫法更加寬松。

那么問題來了,移除 key 照理說應(yīng)該是移除對(duì)象里面的 key ,即 K extends keyof any 應(yīng)該改為 K extends keyof T,TS 沒有這么做的道理是啥捏?

原因我想破了腦袋也沒想到,去 Github 搜了下,發(fā)現(xiàn)大家普遍的需求是讓 Omit 的寫法更嚴(yán)謹(jǐn),TS 官方答應(yīng)著,并沒實(shí)現(xiàn),點(diǎn)擊了解 "Omit" type using "keyof any" instead of "keyof T"

如果說,我們想要一個(gè)嚴(yán)格的 Omit,我們可以把 Omit 的 K extends keyof any 改為 K extends keyof T 自己實(shí)現(xiàn)一個(gè)較為嚴(yán)格的 Omit,我們叫它 Remove ,源碼:

type Remove<T, K extends keyof T> = { [P in Exclude<keyof T, K>]: T[P]; }

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

type noAge = Remove<IPerson, "age">; // yes type noAge = { name: string; }
type noRandomKey = Remove<IPerson, "灰機(jī)">; // no

NonNullable<Type>

NonNullable 通過從類型中排除 null 和 undefined 來創(chuàng)建新類型。

基本上,它是 Exclude<T,null | undefined> 的縮寫:

type Type = string | null | undefined; 

// "string"
type NonNullableType = NonNullable<Type>;

Parameters<Type>

參數(shù)從函數(shù)類型 Type 的參數(shù)中使用的類型構(gòu)造元組類型:

const addNumbers = (x: number, y: number) => {
  return x + y;
};

// [x: number, y: number]
type FunctionParameters = Parameters<typeof addNumbers>;

使用 addNumbers 的時(shí)候?yàn)槭裁催€要加上 typeof 呢?因?yàn)?addNumbers 是 JS 代碼實(shí)現(xiàn),我們需要的是函數(shù)簽名,所以加上 typeof ,如果我們直接給一個(gè)函數(shù)簽名,就不需要加上 typeof ,例如:

type addNumbers = (x: number, y: number) => number;

// [x: number, y: number]
type FunctionParameters = Parameters<addNumbers>;

您還可以檢索單個(gè)參數(shù):

const addNumbers = (x: number, y: number) => {
  return x + y;
};

// "number"
type FirstParam = Parameters<typeof addNumbers>[0];

// "number"
type SecondParam = Parameters<typeof addNumbers>[1];

// "undefined"
type ThirdParam = Parameters<typeof addNumbers>[2];

如果獲取函數(shù)參數(shù)的類型以確保類型安全很有用,尤其是在外部使用時(shí):

const saveUser = (user: { name: string; surname: string; age: number }) => {
  // ...
};

const user: Parameters<typeof saveUser>[0] = {
  name: "John",
  surname: "Doe",
  age: 18,
};

源碼展示,仔細(xì)看這個(gè)條件泛型,尤其是 infer R

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

ConstructorParameters<Type>

ConstructorParameters 根據(jù)構(gòu)造函數(shù)的類型構(gòu)造元組或數(shù)組類型。

基本上,它類似于參數(shù),但適用于類構(gòu)造函數(shù):

class UserManager {
  private name: string;
  private surname: string;

  constructor(user: { name: string; surname: string }) {
    this.name = user.name;
    this.surname = user.surname;
  }
}

// "[user: { name: string, surname: string} ]"
type UserManagerConstructorParams = ConstructorParameters<typeof UserManager>;

與 Parameters 類型相同,當(dāng)我們外部使用時(shí),它有助于確保構(gòu)造函數(shù)接受我們的參數(shù):

class UserManager {
  private name: string;
  private surname: string;

  constructor(user: { name: string; surname: string }) {
    this.name = user.name;
    this.surname = user.surname;
  }
}

const params: ConstructorParameters<typeof UserManager>[0] = {
  name: "John",
  surname: "Doe",
};

源碼展示:

type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;

ReturnType<Type>

ReturnType 構(gòu)造函數(shù)Type的返回類型的類型:

const getUser = () => ({
  name: "John",
  surname: "Doe",
  age: 18,
});

// { name: string; surname: string; age: number; }
type FunctionReturnType = ReturnType<typeof getUser>;

與 Parameters 和 ConstructionParameters 一樣,當(dāng)您外部使用并希望獲得導(dǎo)入函數(shù)的返回類型時(shí),它很有用:

const getUser = () => ({
  name: "John",
  surname: "Doe",
  age: 18,
});

type User = ReturnType<typeof getUser>;

const user: User = {
  name: "Andrew",
  surname: "Hopkins",
  age: 20,
};

源碼展示:

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

InstanceType<Type>

InstanceType構(gòu)建一個(gè)類型包括實(shí)例類型的構(gòu)造函數(shù)的類型。

基本上,它類似于 ReturnType,但作用于類構(gòu)造函數(shù):

class UserManager {
  name: string;
  surname: string;

  constructor(user: { name: string; surname: string }) {
    this.name = user.name;
    this.surname = user.surname;
  }
}

// { name: string; surname: string }
type UserMangerInstanceType = InstanceType<typeof UserManager>;

您可能不會(huì)這樣做,因?yàn)槟梢灾苯邮褂?UserManager 類型:

class UserManager {
  name: string;
  surname: string;

  constructor(user: { name: string; surname: string }) {
    this.name = user.name;
    this.surname = user.surname;
  }
}

const user2: UserManager = {
  name: "John",
  surname: "Doe",
};

源碼展示:

type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;

ThisParameterType<Type>

提取函數(shù) this 的類型,若函數(shù)類型并沒有此參數(shù),則提取為 unknown 類型。

function toHex(this: Number) {
  return this.toString(16);
}

function numberToString(n: ThisParameterType<typeof toHex>) {
  return toHex.apply(n);
}

因?yàn)?this 指向的問題,項(xiàng)目中并不常用.

源碼展示:

type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any ? U : unknown;

最后

以上源碼展示,均可在 TypeScript 項(xiàng)目的 ./node_modules/typescript/lib/lib.es5.d.ts 路徑找到。

因?yàn)楣俜轿臋n實(shí)施更新的緣故,此文章可能過時(shí),請(qǐng)以官方文檔為準(zhǔn):utility-types

最后編輯于
?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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