TypeScript 中的 Extends 怎么那么優(yōu)秀?。?/h1>
在學(xué)習(xí)和使用 ts 的時(shí)候,有一個(gè)語(yǔ)法會(huì)大量的出現(xiàn),他就是 extends。但是這個(gè)語(yǔ)法放到 ts 里,就顯得非常怪異,因?yàn)楹枚鄷r(shí)候跟我們常規(guī)的理解看上去好像不太一樣,不就是一個(gè)繼承嗎,咋到處都在亂用???
實(shí)際上,之所以怪,是因?yàn)樵?ts 中,extends 不只是要表達(dá)繼承的意思,他還有一些延展含義。
在 JS 核心進(jìn)階中,我們?cè)趯W(xué)習(xí)設(shè)計(jì)模式的時(shí)候,曾經(jīng)提高過一個(gè)原則:里氏替換原則,該原則針對(duì)的是父類與子類之間的替換關(guān)系:任何使用父類實(shí)例的地方,都能夠使用子類實(shí)例完美替換。
class Person {
constructor(name) {
this.name = name
}
run(t) {
console.log(`${this.name} 跑了 ${t} 公里`);
}
}
class Student extends Person {
constructor(name, grade) {
super(name)
this.grade = grade
}
}
const p1 = new Person('Tom')
p1.run(20)
const s1 = new Student('Tom')
s1.run(20)
這個(gè)案例中,我們能夠使用 s1 去替換 p1。而不會(huì)出現(xiàn)什么問題。
在 ts 的類型兼容性里,也符合這個(gè)原則?;谶@個(gè)邏輯,我們就可以把 extends 作為一個(gè)判斷條件,來(lái)驗(yàn)證你是否合理運(yùn)用了里氏替換原則,從而衍生出它新的用法。
一、繼承
繼承的運(yùn)用非常的常規(guī)。在面向?qū)ο蟮倪\(yùn)用中,我們可以繼承一個(gè)父類。
class Parent {}
class Children extends Parent {}
我們也可以在 interface 的類型聲明中,使用繼承。
interface Animal {
kind: string
}
interface Dog extends Animal {
bark(): void
}
它等價(jià)于。
interface Dog {
kind: string
bark(): void
}
二、泛型約束
我們先簡(jiǎn)單來(lái)看一下這個(gè)東西是如何在泛型中使用的,然后再來(lái)結(jié)合里氏替換原則來(lái)分析它的邏輯。
interface Dispatch<T extends { type: string }> {
(action: T): T
}
我們?cè)诙x Dispatch 時(shí)需要傳入一個(gè)泛型,傳入的泛型類型必須與 {type: string} 符合里氏替換原則。意思就是說,要傳入該類型的子類型。
因此,我們可以傳入。
var action = {
type: 'get/list',
playload: 10
}
也可以傳入。
var action = {
type: 'merge'
}
var action = {
type: 'add',
value: { a: 1, b: 2 }
}
從結(jié)論上來(lái)看,父類型的約束力度更小,子類型的約束力度更大。
三、條件判斷
我們可以可以繼續(xù)衍生,當(dāng)子類型與父類型符合正常的繼承關(guān)系時(shí),判斷結(jié)果為 true,否則為 false。
這里的繼承關(guān)系,表達(dá)的是一種替換關(guān)系,或者說是約束力度的縮小。
type C = A extends B ? string : number
這里表達(dá)的含義是,當(dāng) A 能夠替換 B 時(shí),判斷結(jié)果為 true,否則,判斷結(jié)果為 false。
interface Person {
name: string
}
interface Yung extends Person {
gender: string
}
interface Student extends Yung {
age: string
}
也就是說,當(dāng) A 作為 B 的子類型時(shí),判斷結(jié)果為 true。
// 此時(shí)判斷結(jié)果為true
type C = Yung extends Person ? number : string // number
// 此時(shí)判斷結(jié)果為false
type C = Yung extends Student ? number : string // string
也可以結(jié)合泛型使用。
type P<T> = T extends string ? string : number
type Z = P<string> // string
當(dāng)我們?cè)谑褂梅盒偷臅r(shí)候,會(huì)出現(xiàn)一些問題,看一下這個(gè)例子。
type A = number | string extends string ? string : number // number
因?yàn)?string 的約束力度,比 number | string 更大,因此這里的條件判斷為 false,但是當(dāng)我們通過泛型來(lái)做到同樣的事情時(shí),情況就發(fā)生了變化。
type P<T> = T extends string ? string : number
type A = P<number | string> // string | number
當(dāng)我們用泛型傳遞時(shí)候,跟預(yù)想中的不太一樣,這里會(huì)把泛型傳入的 number 和 string 拆分之后在去運(yùn)行 extends 判斷。因此最后的結(jié)果是 string | number。
聯(lián)合類型在泛型中的表現(xiàn)是分配之后再傳入。
在實(shí)踐中一定要警惕這個(gè)小小的差異。我們可以使用如下的方式避免這種先分配再傳入的規(guī)則。
type P<T> = [T] extends [string] ? string : number
type A = P<number | string> // number
never 表示所有類型的子類型,因此也被看成是一個(gè)聯(lián)合類型,當(dāng)我們?cè)诜盒椭袀魅?nbsp;never 時(shí)也會(huì)同理出現(xiàn)同樣的問題。
type P<T> = T extends string ? string : number
// 沒有類型可分配,直接返回 never
type A = P<never> // never
注意他們的不同。
type P<T> = [T] extends [string] ? string : number
type A = P<never> // string
四、定義一個(gè) pick
現(xiàn)有一個(gè)對(duì)象 A 有很多個(gè)屬性,我希望重新定義一個(gè)新的對(duì)象 B,該對(duì)象的屬性是從 A 里挑選出來(lái)的,那么 B 的類型應(yīng)該怎么定義呢。
interface A {
name: string;
age: number;
gender: number;
class: string
}
當(dāng)然,我們可以用常規(guī)的方式來(lái)定義,不過有的時(shí)候這樣會(huì)比較麻煩。
interface B {
name: string,
age: number
}
我們也可以利用泛型和 extends,定義一個(gè) Pick 類型。
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}
type B = Pick<A, 'name' | 'age'>
當(dāng)我們?cè)?Pick 中傳入 A 時(shí), keyof A 的結(jié)果為 name | age | gender | class,因此 'name' | 'age' 是 keyof A 的子類型。
此時(shí)的 B 得到與上面寫法一樣的結(jié)果。
五、定義一個(gè) Exclude
現(xiàn)在我有一個(gè)聯(lián)合類型。
type a = 'name' | 'age' | 'gender' | 'class'
我希望排除其中一個(gè) name,得到一個(gè)新的聯(lián)合類型。
type b = 'age' | 'gender' | 'class'
此時(shí)我們可以定一個(gè)排除的泛型類型來(lái)做到這個(gè)事情。
type b = Exclude<a, 'name'>
這個(gè) Exclude 是如何實(shí)現(xiàn)的呢?非常的簡(jiǎn)單。
type Exclude<T, U> = T extends U ? never : T
type b = Exclude<a, 'name'>
我們來(lái)分析一下,首先剛才我們已經(jīng)知道,當(dāng)傳入的泛型為聯(lián)合類型時(shí),會(huì)先分配再傳入。
因此,此時(shí)傳入的聯(lián)合類型 a 會(huì)被拆分傳入。
也就是說,T exnteds U 的比較會(huì)變成。
// never
'name' extends 'name' ? never : 'name'
// age
'age' extends 'name' ? never : 'age'
// gender
'gender' extends 'name' ? never : 'gender'
// class
'class' extends 'name' ? never : 'class'
所以通過這種方式,我們可以做到從聯(lián)合類型中排除指定的類型。
六、定義一個(gè) Omit
Omit 是 Pick 的取反,表示挑選剩余的屬性組成新的對(duì)象。理解了 Pick 和 Exclude,這個(gè)理解起來(lái)非常容易。
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>
使用:
interface A {
name: string,
age: number,
gender: number,
class: string
}
type B = Omit<A, 'name'>
等價(jià)于:
interface A {
age: number,
gender: number,
class: string
}
大家可以自己分析一下 Omit 的實(shí)現(xiàn)原理,應(yīng)該是沒有任何難度的。
七、最后
最后來(lái)個(gè)騷的,大家分析一下這玩意兒有什么用
type TypeString<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";