Typescript 高階類型

大部分示例和教程來自優秀的 A. Sharif 的系列文章html

泛型 和 extends

function prop(obj, key) {
  return obj[key]
}
複製代碼

這樣一個函數如何讓其類型安全呢?typescript

function prop<T, Key extends keyof T>(obj: T, key: Key) {
  return obj[key];
}

const user: User = {
  id: 1,
  name: "Test User",
  points: 0
};

const userName = prop(user, "name"); // const userName : string;
複製代碼

須要像這樣使用到泛型,由於咱們不知道這個 obj 的類型定義是怎樣的,因此咱們須要使用 T 來做爲 obj 的定義的佔位,T 是第一個泛型參數。Key 是第二個泛型參數,它 extends 自 T 的全部 key 值,Key 在這裏就是 "id" | "name" | "points" 的全部子集。express

在使用的時候咱們能夠不用顯式的傳入泛型參數,由於咱們給函數傳參的時候,TS 編譯器就已經幫咱們推導出來正確的泛型參數了,而且驗證了第二個參數的類型必須是 extends 自 T 的全部 key 值。這就爲咱們提供了魯棒性很是好的,類型安全的函數了,結合 vscode 還有自動補全的功能哦。json

Conditional Types

條件類型自 Typescript 2.8 引入。 它的定義是安全

"A conditional type selects one of two possible types based on a condition expressed as a type relationship test"app

type S = string extends any ? "string" : never;

/* type S = "string" */
複製代碼

這個三元操做符的解釋是若是 string 類型 extends 自 any 類型,(答案是明顯的YES)。那麼類型 S 的定義就是 string,不然就是 never函數

常見的應用就是標準庫中 Exclude 的定義。學習

/** * Exclude from T those types that are assignable to U */
type Exclude<T, U> = T extends U ? never : T;
複製代碼

Exclude可用於兩個聯合類型之間取差集。ui

Pick 和 Omit

lodash 庫中有兩個實用的方法,pickomit.es5

var object = { 'a': 1, 'b': '2', 'c': 3 };
 
_.pick(object, ['a', 'c']);
// => { 'a': 1, 'c': 3 }
複製代碼

pick 用於由選取的對象屬性組成新的對象。

var object = { 'a': 1, 'b': '2', 'c': 3 };
 
_.omit(object, ['a', 'c']);
// => { 'b': '2' }
複製代碼

omit 的釋義是消除。與 pick 相反; 此方法建立一個對象,消除指定的屬性,剩下的對象屬性組成一個新的對象。

Pick

在 Typescript 中也有相似的概念。只不過被操做的對象不是對象字面量,而是類型.

Pick 在 Typescript 的標準庫中的定義是

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

能夠看出 Pick 也是一種類型,可用於創造新的類型。這個新的類型的 key 值爲泛型 T 的全部 key 值的聯合類型的子集。

interface Person {
    name: string;
    age: number;
    location: string;
}
type PersonWithouLocation = Pick<Person, 'name' | 'age'>
// => type PersonWithouLocation = {
// name: string;
// age: number;
// }
複製代碼

那如何爲 _.pick 加上類型呢?

declare function pick<T extends object, K extends keyof T>(object: T, paths?: K[]): Pick<T, K> var object = { 'a': 1, 'b': '2', 'c': 3 };
const result = pick(object, ['a', 'c']); 
// const result: Pick<{
// 'a': number;
// 'b': string;
// 'c': number;
// }, "a" | "b">
複製代碼

使用泛型和 Pick ,咱們就獲得了一個類型安全的 pick 方法

Omit

在 Typescript 3.5 版本才引入 Omit 類型。

Omit 做爲 Pick 的反操做,咱們想獲得和上面 Pick 相同的操做只須要對類型的鍵值取反便可。

interface Person {
    name: string;
    age: number;
    location: string;
}
type PersonWithouLocation = Omit<Person, 'location'>
// 結果是
// type PersonWithouLocation = {
// name: string;
// age: number;
// }
複製代碼

Omit 在標準庫中的定義是

/** * Construct a type with the properties of T except for those in type K. */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
複製代碼

讓咱們看看 Omit 的實現,就是 Pick 出 T 中 keyof T 和 K 的差集組成新的類型。也就是說消除了第二個參數中爲鍵值的類型組成新的類型。

Omit 實現中的第二個泛型參數爲何不是 K extends keyof T,而是使用的 K extends keyof any呢?這樣 IDE 就不能在第二個參數幫助補全了,這是個人一個疑問。在 Stack Overflow 上有找到 key 值多是 string | number | symbol 三種類型。

那如何爲 _.omit 加上類型呢?

declare function omit<T extends object, K extends keyof T>(object: T, paths: K[]): Omit<T, K>

const result = omit(object, ['a', 'b']); 
// const result: Pick<{
//     'a': number;
//     'b': string;
//     'c': number;
// }, "c">
複製代碼

擴展一下,也許某些時候 omit 方法的第二個參數多是擴展運算符。例如

function omit(object, ...rest) {
    // do omit
}
omit(object, 'a', 'b')
複製代碼

那這個時候的類型定義應該是這樣

declare function omit<T extends object, K extends keyof T>(object: T, ...paths: K[]): Omit<T, K> 複製代碼

infer

infer 是 TS 的一個關鍵字,用於顯式類型推斷。與之相關的常見的兩個類型就是 ReturnTypeParameters

function getInt(a: string) {
  return parseInt(a);
}

type A = ReturnType<typeof getInt>; // => number
複製代碼

再這個例子中,咱們先須要使用 typeof 關鍵字獲取函數的類型定義,也就是 (a: string) => number, 而後再將其做爲泛型參數傳入 ReturnType。這樣就能從一個函數聲明獲得它的返回類型,這能夠在寫代碼的時候減小咱們一些心智負擔,可以很是靈活的獲取類型。

ReturnType 在標準庫中的實現是

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
複製代碼

對這個實現的解釋是,若是類型 T 是擴展自函數類型,那麼返回值就是 infer 關鍵字推斷出的 R 類型,若是不是,就返回 any 類型。

根據這個實現咱們還能實現一個高階類型用於推斷函數參數。

type ParametersType<T> = T extends (...args: infer K) => any ? K : any;
複製代碼

使用這個類型推斷出的類型是參數元組類型。

其實這個類型在標準庫中的的實現是這樣的,只不過這個標準庫實現了限定了泛型入參的類型必須爲函數類型。

/** * Obtain the parameters of a function type in a tuple */
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
複製代碼

lib.es5.d.ts 中還實現了不少相似的高階類型。可參考 官網

Mapped types 映射類型

常見的 Mapped types 都很簡單, 能夠簡單過一下

Readonly

假設咱們有以下類型。

type User = {
  readonly id: number;
  name: string;
}
複製代碼
type Readonly<Type> = {readonly [key in keyof Type ]: Type[key]};
type ReadonlyUser =  Readonly<User>;
複製代碼

Readonly 的實現就是讓泛型的每個 key 都用 readonly 關鍵字標註,這樣就能獲得每個 key 都是 readonly 只讀的類型了。

Partial

/** * Make all properties in T optional */
type Partial<T> = {
    [P in keyof T]?: T[P];
};
複製代碼

同理使用 ? 讓每個 key 均可選。

type BlogPost = {
  id: number;
  title: string;
  description?: string;
}

type PartialBlogPost = Partial<BlogPost>;
/* => type PartialBlogPost { id?: number | undefined; title?: string / undefined; description?: string / undefined; } */
複製代碼

Required

/** * Make all properties in T required */
type Required<T> = {
    [P in keyof T]-?: T[P];
};
複製代碼

Required 類型和上兩個差很少,須要注意的是 - 這個修飾符,它意味着去除後面的可選修飾符 ?, 那就是每個 key 都是必需。同理也存在 + 修飾符。

type BlogPost = {
  id: number;
  title: string;
  description?: string;
}

type RequiredBlogPost = Required<BlogPost>;
/* => type RequiredBlogPost { id: number; title: string; description: string; } */
複製代碼

Record

在剛開始學習 TS 的時候,Record 的實現和使用場景讓我比較困惑。

/** * Construct a type with a set of properties K of type T */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};
複製代碼
type ExportFormat = "jsonMiniDiary" | "md" | "pdf" | "txtDayOne";
const fileExtensions: Record<ExportFormat, string> = {
	jsonMiniDiary: "json",
	md: "md",
	pdf: "pdf",
	txtDayOne: "txt",
};
複製代碼

這是一段來自 real world 的代碼。經過這段代碼,能看到 Record 可以幫助咱們得到類型安全的對象聲明。

待持續更新,TS 的類型世界真複雜。

相關文章
相關標籤/搜索