構造類型抽象、TypeScript 編程內參(二)

本文是《TypeScript 編程內參》系列第二篇:構造類型抽象,主要記述 TypeScript 的高級使用方法和構造類型抽象。編程

PS: 本文語境下的「約束」指的是「類型對值的約束」數組

1、構造類型抽象

在 TS 的世界裏,總有「動態地生成類型」的需求,好比下面的 UserWithHisBlogsUser 重複的部分:編程語言

type User = {
    id: number;
    name: string;
}

type UserWithHisBlogs = {
    id: number;
    name: string;
    blogs: Blog[]
}

type Blog = {
    id: number;
    content: string;
    title: string;
}
複製代碼

上面的類型定義是存在冗餘的,當 User 上面寫了新的字段,咱們就不得不手工的去改 UserWithHisBlogs 以使其擁有剛剛新增的字段。函數

那麼,有沒有什麼抽象的方法避免這個問題呢?有的, 利用 &:post

type UserWithHisBlogs = User & {
    blogs: Blog[];
}
複製代碼

這裏後文會解釋 & 的含義,這裏的 UserWithHisBlogs 跟一開始的例子裏的類型徹底等價,惟一的區別是利用 & 關聯了 User,避免了上面那個重複修改的問題。ui


這裏只是個簡單的引子,抽象的意義在於減小重複的事情,類型抽象的意義在於減小冗餘的類型說明(減小重複的類型說明)spa

在實際 TS 編程的時候應該特別注意:經過構造類型抽象,儘可能複用原有的類型聲明,避免重複聲明。prototype

2、構造數/元組類型

咱們能夠這樣聲明數組的類型:3d

type Arr = Array<any>;
// 這樣也能夠,跟上面幾乎是等價的
type Arr = any[];
複製代碼

可是這樣聲明的數組元素類型都是同樣的,不少狀況下咱們代碼裏面的數組裏不一樣位置的元素的類型是不同的,所以有了元組這樣的類型抽象去約束數組元素:code

type NumStr = [number, string];
// 第三個元素不在 NumStr 裏,會報錯
const pair: NumStr = [1, '1', 'xxx'];

// 能夠嵌套聲明
type N = [[number, string], [number, string]]
const n: N = [[1, '1'], [2, '2']];
複製代碼

3、構造聯合/交叉類型

ts 的類型是能夠計算的,經過不一樣的運算符鏈接不一樣的類型能夠得出不一樣的類型。


聯合類型 Uinion Type 一般由 | 運算符鏈接兩個類型得出來的,如 A | B 的意思是要麼知足 A 的約束,要麼知足 B 的約束 (知足一個便可)

能夠參考下面的例子:

type Suffix = '先生' | '女士';
const sayName = (name: string, suffix: Suffix) => {
    return name + ' ' + suffix;
}
sayName('e', '先生');
sayName('c', '女士');
sayName('z', '老師'); // 報錯
複製代碼

用約束的視角來看待類型計算會容易不少

交叉類型 Intersection Type 一般由 & 運算符鏈接兩個類型得出來的,如 A & B 的意思是既要知足 A 的約束,也要知足 B 的約束 (同時知足)

實例參考:

type Admin = { permission: 100 };
type User = { permission: number, name: string };

function systemReboot(user: User & Admin) {
    if (user.permission < 100) {
        throw new Error('systemReboot error: permission deny');
    } else {
        /** $ sudo reboot **/
    }
}

systemReboot({
    permission: 1, // 這裏不知足 Admin 的約束 報錯哦
    name: '普通用戶'
});

systemReboot({
    permission: 100, // 能夠 ~
    name: '管理員用戶'
});

// 有了交叉類型咱們便沒必要定義 AdminUser
type AdminUser = { permission: 100, name: string  };
// 取而代之的是 (避免了重複聲明、儘量的利用現有元素來構造新的類型)
type AdminUser = Admin & User;
複製代碼

看完上面的例子,估計不少人都會想到,能不能定義偶數這種類型?以目前 ts 的能力來看,如今還不具有基本類型的動態拆解能力,或許將來會有,可是 ts 如今能夠作到對象的動態拆解/抽象哦,後文會詳細描述。

4、構造 never 類型

瞭解聯合和交叉類型後,聰明的你也許已經發現了相似這樣的類型表達式:

type WTF = 'A' & 'B';
複製代碼

既是字符串 'A' 又是字符串 'B' 的「薛定諤的值」?顯然,js 裏是不存在這樣的值的

經過 VSCode 咱們能夠看到這裏的 WTF 類型是 never 其含義是 any 的對立面,即「什麼值都不兼容」或者「沒有」:

  1. any: 無約束 => 什麼值均可以兼容
  2. never: 無窮強的約束 => 什麼值都不兼容

如下是對「什麼值都不兼容」的代碼說明

let n: never;
n = e;
n = 'e'
n = {};
n = () => {};
n = n;
// never 跟任何類型都不兼容,除了它自己
複製代碼

💡💡💡 never 並不是一無可取,在後文的一些高級用法裏 never 很常見。


關注【IVWEB社區】公衆號查看最新技術週刊,今天的你比昨天更優秀!


5、利用 extends 拓展類型

extends 拓展了 ES 原生的 extends,在 ts 的語境下,A extends B 意思是既要 A 繼承自 B,其做用相似於 & :

interface AdminUser extends User { permission: 100 };
interface User { permission: number, name: string };

function systemReboot(user: AdminUser) {
    if (user.permission < 100) {
        throw new Error('systemReboot error: permission deny');
    } else {
        /** $ sudo reboot ... **/
    }
}

systemReboot({
    permission: 1, // 這裏不知足 Admin 的約束 報錯哦
    name: '普通用戶'
});

systemReboot({
    permission: 100, // 能夠 ~
    name: '管理員用戶'
});
複製代碼

此外,extends 還能夠用來約束泛型的範圍:

interface HasName {
    name: string;
}

// 這裏的意思是 T 做爲泛型的話首先要知足 HasName
function sayObjName<T extends HasName>(obj: T) {
    console.log(obj.name); // 不會報錯
}

sayObjName({ name: 'eczn' });

sayObjName({});
// 類型不和報錯,
// 由於在這裏 T 的類型是 {}
// 它並不知足 HasName 的約束
複製代碼

6、構造對象索引

在實際代碼運行的過程當中,咱們老是有這樣的一種需求

有這樣的一種對象 Map:其鍵是某個惟一 Key,它對應的值是這個 Key 表明的對象

也就是說須要定義「對象的鍵和值」

在這種狀況下,咱們能夠爲這種「對象」聲明它的「索引類型」以達到咱們的要求:

interface User {
    uid: string;
    name: string;
}

interface ObjMap {
    // 這意思是對象鍵名類型爲 string 其對應的值類型爲 User
    [uid: string]: User;
    // string => User
}

const map: ObjMap = {
    'eczn': { uid: 'eczn', name: '喵嗚' },
    'eeee': 'ggg' // 不知足,報錯
}
複製代碼

💡💡💡 若是你喜歡用 Array.prototype.reduce 規約數組的話,對象索引會用的比較多

7、利用 keyof 構造鍵名聯合

keyof 是 ts 提供的類型運算符,用於取出對象類型的鍵名聯合,返回的結果是一個聯合類型:

interface Person {
    name: string;
    age: number;
    sex: 0 | 1; // 0 表明女士;1 表明男士
}

type KeyOfPerson = keyof Person;
// 'name' | 'age' | 'sex'
// 這裏 KeyOfPerson 的意思是多是 'name' 多是 'age' 多是 'sex'

const personKey: KeyOfPerson = 'xxx';
// ^^^^^ 報錯 xxx 並非 Person 鍵
複製代碼

利用 keyof,能夠很容易的遍歷一個對象的字段,並在原對象的基礎上生成新的對象:

// 下面的這個類型會把 T 上面的字段對應的值所有設置爲 number
type ObjToNum<T> = {
    [key in keyof T]: number;
}

type Person = {
    name: string;
    address: string;
}

type Test = ObjToNum<Person>;
// Test = { name: number, address: number }
複製代碼

ObjToNum 中 key in keyof T 的意思是說, 遍歷 keyof T 裏的元素做爲 key, 將這些 key 做爲鍵,並將這些鍵所對應的值類型設置爲 nunber。

考慮到 key in keyof T,中的 keyof T 能夠是任意的聯合類型或字面量,所以能夠很容易的寫出相似下面這樣的類型 JustNameAge:

// HasNameAge 用於約束泛型
interface HasNameAge {
    name: any,
    age: any
}

// 將 T 裏面的 name 和 age 單獨挖出來做爲新類型
// (這個新類型是 T 的子集)
type JustNameAge<T extends HasNameAge> = {
    // key 是變量 T[key] 也就很容易理解了
    [key in 'name' | 'age']: T[key]

    // 固然, 最好這樣寫 (減小冗餘)
    // [key in keyof HasNameAge]: T[key]
}

// Test1 => { name: string, age: number }
type Test1 = JustNameAge<{
    name: string,
    age: number,
    sayName: () => void
}>;

// 下面這個會報錯
// 由於其泛型入參不知足 HasNameAge 約束
type Test2 = JustNameAge<{
    name: string
}>;
複製代碼

8、構造條件類型 Conditional Types

有時候,咱們須要去除一個對象的函數項 ... 這裏可能須要通常的編程語言裏面的 if 判斷來進行類型抽象。

首先,我先聲明一些基礎類型:

// 咱們的問題是:
// 如何將 ABC 中的函數項去除,使其變成 type ABC2 = { a: 1 } ?
type ABC = { a: 1, b(): string, c(): number };

// 若是一個值知足這個約束,則這個值爲一個函數
type AnyFunc = (...args: any[]) => any;
// (也許你也猜到了,我用它來作形如 T extends AnyFunc 的操做)
複製代碼

下一步,咱們利用 ? 並結合 extends 作處理:

// 構造 Test1
// Test1 = { a: "a"; b: never; c: never; }
type Test1 = {
    // 這裏的意思是 ABC[K] 若是知足 AnyFunc 則取出 K,否則取的是 never
    [K in keyof ABC]: (
        ABC[K] extends AnyFunc ? never : K
        // ^^^ 注意這裏拿的是 K
    )
}

// 而後構造 Test2
// Test1[keyof ABC]
// = Test1['a' | 'b' | 'c']
// = 'a' | never | never 
// = 'a'
// ^^^ 注意這裏,對於任意類型 A,never | A 最後等於 A
type Test2 = Test1[keyof ABC];


// 而後咱們在 Test2 的基礎上進行最後一步處理獲得 Test3:
// Test3 = { a: 1 }
type Test3 = {
    [K in Test2]: ABC[K]
}
複製代碼

把上面的推導過程整理一下,能夠獲得 GetStaticFor 用於抽取某對象類型的非函數項:

// 這裏的思想是取出靜態項所對應的 keys
type GetStaticKeysFor<T> = {
    [K in keyof T]: T[K] extends AnyFunc ? never : K
}[keyof T];

// 而後再利用這個 keys 去遍歷原對象來取出對應的鍵值
type GetStaticFor<T> = {
    [K in GetStaticKeysFor<T>]: T[K]
}
複製代碼

9、使用 infer 進行 extends 推斷

有時候,咱們須要將泛型「挖出來」,好比咱們須要獲取到 Promise 類型裏蘊含的值,能夠利用 infer 這樣使用:

type PromiseVal<P> = P extends Promise<infer INNER> ? INNER : P;

type PStr = Promise<string>;

// Test === string
type Test = PromiseVal<PStr>;
複製代碼

此外,infer 只能跟在 extends 的後面出現,由於只有 extends 的語境下,才能體現 infer 的語義:動態地給類型的某個結構命名 以便在後續的 TRUE 分支裏面使用。

Array 也能夠:

type ArrayVal<P> = P extends Array<infer INNER> ? INNER : P;

// Test ==> string | number
type Test = ArrayVal<[string, number]>;
複製代碼

10、本篇末

本篇主要講述的是如何構造類型抽象以便描述/生成更多的類型,如下是 Checklist:

  1. 掌握本篇當中描述的各類類型抽象方法
  2. 能熟練使用範型、熟練的查看其餘人寫的類型定義
  3. 經過搭配不一樣簡單抽象來構造更復雜的抽象
  4. 利用類型抽象減小業務代碼中類型標註的冗餘性,減小重複工做

本文的下一篇是「工程化和運行時、TypeScript 編程內參(三)」,敬請期待

相關連接: 約束即類型、TypeScript 編程內參(一)

相關文章
相關標籤/搜索