您好,登錄后才能下訂單哦!
本文小編為大家詳細介紹“typeScript的extends關鍵字怎么使用”,內容詳細,步驟清晰,細節處理妥當,希望這篇“typeScript的extends關鍵字怎么使用”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學習新知識吧。
讓我們開門見山地說吧,在 typeScript 在不同的上下文中,extends
有以下幾個語義。不同語義即有不同的用途:
用于表達類型組合;
用于表達面向對象中「類」的繼承
用于表達泛型的類型約束;
在條件類型(conditional type)中,充當類型表達式,用于求值。
extends
可以跟 interface
結合起來使用,用于表達類型組合。
示例 1-1
interface ChildComponentProps { onChange: (val: string)=> void } interface ParentComponentProps extends ChildComponentProps { value: string }
在 react 組件化開發模式中,存在一種自底向上的構建模式 - 我們往往會先把所有最底層的子組件的 props
構建好,最后才定義 container component
(負責提升公共 state,聚合和分發 props) 的 props
。此時,inferface 的 extends
正好能表達這種語義需求 - 類型的組合(將所有子組件的 props
聚合到一塊)。
當然,interface
的 extends
從句是可以跟著多個組合對象,多個組合對象之間用逗號,
隔開。比如 ParentComponentProps
組合多個子組件的 props
:
示例 1-2
interface ChildComponentProps { onChange: (val: string)=> void } interface ChildComponentProps2 { onReset: (value: string)=> void } interface ParentComponentProps extends ChildComponentProps, ChildComponentProps2 { value: string }
注意,上面指出的是「多個組合對象」,這里也包括了Class
。對,就是普通面向概念中的「類」。也就是說,下面的代碼也是合法的:
示例 1-3
interface ChildComponentProps { onChange: (val: string)=> void } interface ChildComponentProps2 { onReset: (value: string)=> void } class SomeClass { private name!: string // 變量聲明時,變量名跟著一個感嘆號`!`,這是「賦值斷言」的語法 updateName(name:string){ this.name = name || '' } } interface ParentComponentProps extends ChildComponentProps, ChildComponentProps2, SomeClass { value: string }
之所以這也是合法的,一切源于一個特性:在 typeScript 中,一個 class 變量既是「值」也是「類型」。在interface extends class
的上下文中,顯然是取 class 是「類型」的語義。一個 interface extends
另外一個 class,可以理解為 interface 拋棄這個 class 的所有實現代碼,只是跟這個 class 的「類型 shape」 進行組合。還是上面的示例代碼中,從類型 shape 的角度,SomeClass
就等同于下面的 interface:
示例 1-4
interface SomeClass { name: string updateName: (name:string)=> void }
好了,以上就是 extends
關鍵字的「類型組合」的語義。事情開始發生了轉折。
如果某個 interface A 繼承了某個 class B,那么這個 interface A 還是能夠被其他 interface 去繼承(或者說組合)。但是,如果某個 class 想要 implements
這個 interface A,那么這個 class 只能是 class B 本身或者 class B 的子類。
示例 1-5
class Control { private state: any; constructor(intialValue: number){ if(intialValue > 10){ this.state = false }else { this.state = true } } checkState(){ return this.state; } } interface SelectableControl extends Control { select(): void; } // 下面的代碼會報錯:Class 'DropDownControl' incorrectly implements interface // 'SelectableControl'. // Types have separate declarations of a private property 'state'.(2420) class DropDownControl implements SelectableControl { private state = false; checkState(){ // do something } select(){ // do something } }
要想解決這個問題,class DropDownControl
必須要繼承 Control
class 或者Control
class 的子類:
示例 1-6
class Control { private state: any; constructor(intialValue: number){ if(intialValue > 10){ this.state = false }else { this.state = true } } checkState(){ return this.state; } } interface SelectableControl extends Control { select(): void; } // 下面的代碼就不會報錯,且能得到預期的運行結果 class DropDownControl extends Control implements SelectableControl { // private state = false; //checkState(){ // do something //} select(){ // do something } } const dropDown = new DropDownControl(1); dropDown.checkState(); // Ok dropDown.select(); // Ok
上面這個示例代碼扯出了 extends
關鍵字的另外一個語義 - 「繼承」。當extends
用于 typeScript 的類之間,它的準確語義也就是 ES6 中面向對象中「extends」關鍵字的語義。AClass extends BClass
不再應該解讀為「類型的組合」而是面向對象編程中的「AClass 繼承 BClass」和「AClass 是父類 BClass 的子類」。與此同時,值得指出的是,此時的 extends
關鍵字是活在了「值的世界」, 遵循著 ES6 中 extends
關鍵字一樣的語義。比較顯著的一點就是,ts 中的 extends
也是不能在同一時間去繼承多個父類的。比如,下面的代碼就會報錯:
示例 1-7
class A {} class B {} // 報錯: Classes can only extend a single class.(1174) class C extends A,B { }
關于具有「繼承」語義的 extends
更多行為特性的闡述已經屬于面向對象編程范式的范疇了,這里就不深入討論了,有興趣的同學可以自行去了解。
至此,我們算是了解 extends
關鍵字跟 interface
和 class
結合起來所表達的兩種不同的語義:
類型的組合
面向對象概念中「類的繼承」
接下來,我們看看用于表達泛型類型約束的 extends
更準確地說,這一節是要討論 extends
跟泛型形參結合時候的「類型約束」語義。在更進一步討論之前,我們不妨先復習一下,泛型形參聲明的語法以及我們可以在哪些地方可以聲明泛型形參。
具體的泛型形參聲明語法是:
標識符后面用尖括號<>
包住一個或者多個泛型形參
多個泛型形參用,
號隔開
泛型新參的名字可以隨意命名(我們見得最多就是使用單個英文字母T
,U
之類的)。
在 typeScript 中,我們可以在以下地方去聲明一個泛型形參。
在普通的函數聲明中:
function dispatch<A>(action: A): A { // Do something }
在函數表達式形態的類型注解中:
const dispatch: <A>(action: A)=> A = (action)=> { return action } // 或者 interface Store { dispatch: <A>(action: A)=> A }
在 interface
的聲明中:
interface Store<S> { dispatch: <A>(action: A)=> A reducer: <A>(state: S,action: A)=> S }
在 class
的聲明中:
class GenericAdd<AddableType> { zeroValue!: AddableType; add!: (x: AddableType, y: AddableType) => AddableType; } let myGenericNumber = new GenericNumber<number>(); myGenericNumber.zeroValue = 0; myGenericNumber.add = function (x, y) { return x + y; };
在自定義類型聲明中:
type Dispatch<A>=(action:A)=> A
在類型推導中: typeScript // 此處,F 和 Rest 就是泛型形參 type GetFirstLetter<S> = S extends `${infer F extends `${number}`}${infer Rest}` ? F : S; 以上就是簡單梳理后的可以產生泛型形參的地方,可能還有疏漏,但是這里就不深入發掘了。
下面重點來了 - 凡是有泛型形參的地方,我們都可以通過 extends
來表達類型約束。這里的類型約束展開說就是,泛型形參在實例化時傳進來的類型實參必須要滿足我們所聲明的類型約束。到這里,問題就來了,我們該怎樣來理解這里的「滿足」呢?在深究此問題之前,我們來看看類型約束的語法:
`泛型形參` extends `某個類型`
為了引出上面所說「滿足」的理解難題,我們不妨先看看下面的示例的代碼:
示例 2-1
// case 1 type UselessType<T extends number> = T; type Test1 = UselessType<any> // 這里會報錯嗎? type Test1_1 = UselessType<number|string> // 這里會報錯嗎? // case 2 type UselessType2<T extends {a:1, b:2}> = T; type Test2 = UselessType2<{a:1, b:2, c:3}> // 這里會報錯嗎? type Test2_1 = UselessType2<{a:1}> // 這里會報錯嗎? type Test2_2 = UselessType2<{[key:string]: any}> // 這里會報錯嗎? type Test2_3 = {a:1, b:2} extends {[key:string]: any} ? true : false // case 3 class BaseClass { name!: string } class SubClass extends BaseClass{ sayHello!: (name: string)=> void } class SubClass2 extends SubClass{ logName!: ()=> void } type UselessType3<T extends SubClass> = T; type Test3 = UselessType3<{name: '鯊叔'}> // 這里會報錯嗎? type Test3_1 = UselessType3<SubClass> // 這里會報錯嗎? type Test3_2 = UselessType3<BaseClass> // 這里會報錯嗎?
不知道讀者朋友們在沒有把上述代碼拷貝到 typeScript 的 playground 里面去驗證之前你是否能全部猜中。如果能,證明你對 extends
在類型約束的語義上下文中的行為表現已經掌握的很清楚了。如果不能,請允許我為你娓娓道來。
相信有部分讀者了解過 typeScript 的類型系統的設計策略。由于 js 是一門動態弱類型的腳本語言,再加上需要考慮 typeScript 與 js 的互操性和兼容性。所以, typeScript 類型系統被設計為一個「structural typing」系統(結構化類型系統)。所謂的結構化類型系統的一個顯著的特點就是 - 具有某個類型 A 的值是否能夠賦值給另外一個類型 B 的值的依據是,類型 A 的類型結構是否跟類型 B 的類型結構是否兼容。 而類型之間是否兼容看重的類型的結構而不是類型的名字。再說白一點,就是 B 類型有的屬性和方法,你 A 類型也必須有。到這里,就很容易引出一個廣為大眾接受的,用于理解類型「可賦值性」行為的心智模型,即:
用集合的角度去看類型。故而這里有「父集」和 「子集」的概念,「父集」包含 「子集」;
在 typeScript 的類型系統中, 子集類型是可以賦值給父集類型。
在泛型形參實例化時,如果 extends
前面的類型是它后面的類型的子集,那么我們就說當前的實例化是「滿足」我們所聲明的類型約束的。
以下是 示例 2-1 的運行結果:
實際上,上面的那個心智模型是無法匹配到以上示例在 typeScript@4.9.4 上的運行結果。以上面這個心智模型(子集類型能賦值給父集類型,反之則不然)來看示例的運行結果,我們會有下面的直覺認知偏差:
case 1 中,any
是 number
的父集,為什么它能賦值給 number
類型的值?
case 1 中,number | string
應該是 number
的父集,所以,它不能賦值給 number
類型的值。
case 1 中,number & string
應該是 number
的父集,按理說,這里應該報錯,但是為什么卻沒有?
case 2 中,{a:1}
是 {a:1,b:2}
的子集,按理說,它能賦值給 {a:1,b:2}
類型的值啊,為什么會報錯?
case 3 中,感覺{name: '鯊叔'}
是 SubClass
的子集,按理說,它能賦值給 SubClass
類型的值啊,為什么會報錯?
case 3 中,感覺BaseClass
是 SubClass
的子集,按理說,它能賦值給 SubClass
類型的值啊,為什么會報錯?
經過反復驗證和查閱資料,正確的認知如下:
case 1 中,any
是任何類型的子集,也是任何類型的父集。這里 typeScript 往寬松方向去處理,即取 number
的子集之意;
number | string
之所以不能賦值給 number
,并不是因為 number | string
是 number
的父集,而是因為聯合類型遇到 extends
關鍵字所產生的「分配律」的結果。即是因為 number|string extends number
的結果等于 (number extend number) | (string extends number)
的結果。顯然,(number string extends number
的值是 false
的,所以,整個類型約束就不滿足;
對象類型的類型不能采用 子集類型 extends 父集類型 = true
的心智模型來理解。而是得采用 父集類型 extends 子集類型 = true
。與此同時,當子集類型中有明確字面量 key-value 對的時候,父集類型中也必須需要有。否則的話,就是不可賦值給子集類型。
number & string
應該被視為對象類型的類型,遵循上面一條的規則。
基于上面的正確認知,我們不妨把我們的心智模型修正一下:
應該使用「父類型」和「子類型」的概念去理解滿足類型約束背后所遵循的規則;
在類型約束 AType extends BType
中,如果 AType
是 BType
的子類型,那么我們就會說 AType
是滿足我們所聲明的類型約束的;
根據下面的 「ts 類型層級關系圖」來判斷兩種類型的父-子類型關系:
注:
1)A -> B
表示「A 是 B 的父類型,B 是 A 的子類型」;
2)strictNullChecks 編譯標志位打開后,undefined
,void
和null
就不會成為 typeScript 類型系統的一層,因為它們是不能賦值給其他類型的。
關于上面這張圖,有幾點可以單獨拿出來強調一下:
any
無處不在。它既是任何類型的子類型,也是任何類型的父類型,甚至可能是任意類型自己。所以,它可以賦值給任何類型;
{}
充當 typeScript 類型的時候,它是有特殊含義的 - 它對應是(Object.prototype.__proto__)=null
在 js 原型鏈上的地位,它被視為所有的對象類型的基類。
array
的字面量形式的子類型就是tuple
,function
的字面量形式的子類型就是函數表達式類型
。tuple
和 函數表達式類型
都被囊括到 字面量類型
中去。
現在我們用這個新的心智模型去理解一下 示例 2-1 報錯的地方:
type Test1_1 = UselessType<number|string>
之所以報錯,是因為在類型約束中,如果 extends
前面的類型是聯合類型,那么要想滿足類型約束,則聯合類型的每一個成員都必須滿足類型約束才行。這就是所謂的「聯合類型的分配律」。顯然,string extends number
是不成立的,所以整個聯合類型就不滿足類型約束;
對于對象類型的類型 - 即強調由屬性和方法所組成的集合類型,我們需要先用面向對象的概念來確定兩個類型中,誰是子類,誰是父類。這里的判斷方法是 - 如果 A 類型相比 B 類型多出了一些屬性/方法的話(這也同時意味著 B 類型擁有的屬性或者方法,A 類型也必須要有),那么 A 類型就是父類,B 類型就是子類。然后,我們再轉換到子類型和父類型的概念上來 - 父類就是「父類型」,子類就是「子類型」。
type Test2_1 = UselessType2<{a:1}>
之所以報錯,是因為{a:1}
是{a:1, b:2}
的父類型,所以是不能賦值給{a:1, b:2}
;
{[key:string]: any}
并不能成為 {a:1, b:2}
的子類型,因為,父類型有的屬性/方法,子類型必須顯式地擁有。{[key:string]: any}
沒有顯式地擁有,所以,它不是 {a:1, b:2}
的子類型,而是它的父類型。
type Test3 = UselessType3<{name: '鯊叔'}>
和 type Test3_2 = UselessType3<BaseClass>
報錯的原因也是因為因為缺少了相應的屬性/方法,所以,它們都不是SubClass
的子類型。
到這里,我們算是剖析完畢。下面總結一下。
當 extends
緊跟在泛型形參后面時,它是在表達「類型約束」的語義;
在 AType extends BType
中,只有 AType
是 BType
的子類型,ts 通過類型約束的檢驗;
面對兩個 typeScript 類型,到底誰是誰的子類型,我們可以根據上面給出的 「ts 類型層級關系圖」來判斷。而對于一些充滿迷惑的邊緣用例,死記硬背即可。
眾所周知,ts 中的條件類型就是 js 世界里面的「三元表達式」。只不過,相比值世界里面的三元表達式最終被計算出一個「值」,ts 的三元表達式最終計算出的是「類型」。下面,我們先來復習一下它的語法:
AType extends BType ? CType : DType
在這里,extends
關鍵字出現在三元表達的第一個子句中。按照我們對 js 三元表達式的理解,我們對 typeScript 的三元表達式的理解應該是相似的:如果 AType extends BType
為邏輯真值,那么整個表達式就返回 CType
,否則的話就返回DType
。作為過來人,只能說,大部分情況是這樣的,在幾個邊緣 case 里面,ts 的表現讓你大跌眼鏡,后面會介紹。
跟 js 的三元表達式支持嵌套一樣,ts 的三元表達式也支持嵌套,即下面也是合法的語法:
AType extends BType ? (CType extends DType ? EType : FType) : (GType extends HType ? IType : JType)
到這里,我們已經看到了 typeScript 的類型編程世界的大門了。因為,三元表達式本質就是條件-分支語句,而后者就是邏輯編輯世界的最基本的要素了。而在我們進入 typeScript 的類型編程世界之前,我們首要搞清楚的是,AType extends BType
何時是邏輯上的真值。
幸運的是,我們可以復用「extends 與類型約束」上面所產出的心智模型。簡而言之,如果 AType
是 BType
的子類型,那么代碼執行就是進入第一個條件分支語句,否則就會進入第二個條件分支語句。
上面這句話再加上「ts 類型層級關系圖」,我們幾乎可以理解AType extends BType
99% 的語義。還剩下 1% 就是那些違背正常人直覺的特性表現。下面我們重點說說這 1% 的特性表現。
我們開門見山地問吧:“請說出下面代碼的運行結果。”
type Test = 1 extends {} ? true : false // 請問 `Test` 類型的值是什么?
如果你認真地去領會上面給出的「ts 類型層級關系圖」,我相信你已經知道答案了。如果你是基于「鴨子辯型」的直觀理解去判斷,那么我相信你的答案是false
。但是我的遺憾地告訴你,在 typeScript@4.9.4中,答案是true
。這明顯是違背人類直覺的。于是乎,你會有這么一個疑問:“字面量類型 1
跟 {}
類型似乎牛馬不相及,既不形似,也不神似,它怎么可能是是「字面量空對象」的子類型呢?”
好吧,就像我們在上一節提過的,{}
在 typeScript 中,不應該被理解為字面量空對象。它是一個特殊存在。它是一切有值類型的基類。ts 對它這么定位,似乎也合理。因為呼應了一個事實 - 在 js 中,一切都是對象 (字面量 1
在 js 引擎內部也是會被包成一個對象 - Number()的實例)。
現在,你不妨拿別的各種類型去測試一下它跟 {}
的關系,看看結果是不是跟我說的一樣。最后,有一個注意點值的強調一下。假如我們忽略無處不在,似乎是百變星君的 any
,{}
的父類型只有一個 - unknown
。不信,我們可以試一試:
type Test = unknown extends {} ? true : false // `Test` 類型的值是 `false`
Test2
類型的值是 false
,從而證明了unknown
是{}
的父類型。
也許你會覺得,extends
與 any
有什么好講得嘛。你上面不是說了「any
」既是所有類型的子類型,又是所有類型的父類型。所以,以下示例代碼得到的類型一定是true
:
type Test = any extends number ? true : false
額......在 typeScript@4.9.4 中, 結果似乎不是這樣的 - 上面示例代碼的運行結果是boolean
。這到底是怎么回事呢?這是因為,在 typeScript 的條件類型中,當any
出現在 extends
前面的時候,它是被視為一個聯合里類型。這個聯合類型有兩個成員,一個是extends
后面的類型,一個非extends
后面的類型。還是用上面的示例舉例子:
type Test = any extends number ? true : false // 其實等同于 type Test = (number | non-number) extends number ? true : false // 根據聯合類型的分配率,展開得到 type Test = (number extends number ? true : false) | (non-number extends number ? true : false) = true | false = boolean // 不相信我?我們再來試一個例子: type Test2 = any extends number ? 1 : 2 // 其實等同于 type Test2 = (number | non-number) extends number ? 1 : 2 // 根據聯合類型的分配率,展開得到 type Test = (number extends number ? 1 : 2) | (non-number extends number ? 1 : 2) = 1 | 2
也許你會問,如果把 any
放在后面呢?比如:
type Test = number extends any ? true : false
這種情況我們可以依據 「任意類型都是any
的子類型」得到最終的結果是true
。
關于 extends 與 any 的運算結果,總結一下,總共有兩種情況:
any extends SomeType(非 any 類型) ? AType : BType
的結果是聯合類型 AType | BType
SomeType(可以包含 any 類型) extends any ? AType : BType
的結果是 AType
在 typeScript 的三元表達式中,當 never
遇見 extends
,結果就變得很有意思了。可以換個角度說,是很奇怪。假設,我現在要你實現一個 typeScript utility 去判斷某個類型(不考慮any
)是否是never
的時候,你可能會不假思索地在想:因為 never
是處在 typeScript 類型層級的最底層,也就是說,除了它自己,沒有任何類型是它的子類型。所以答案肯定是這樣:
type IsNever<T> = T extends never ? true : false
然后,你信心滿滿地給泛型形參傳遞個never
去測試,你發現結果是never
,而不是true
或者false
:
type Test = IsNever<never> // Test 的值為 `never`, 而不是我們期待的 `true`
再然后,你不甘心,你寫下了下面的代碼去進行再次測試:
type Test = never extends never ? true : false // Test 的值為 `true`, 符合我們的預期
你會發現,這次的結果卻是符合我們的預期的。此時,你腦海里面肯定有千萬匹草泥馬奔騰而過。是的,ts 類型系統中,某些行為就是那么的匪夷所思。
對于這種違背直覺的特性表現,當前的解釋是:當 never
充當實參去實例化泛型形參的時候,它被看作沒有任何成員的聯合類型。當 tsc 對沒有成員的聯合類型執行分配律時,tsc 認為這么做沒有任何意義,所以就不執行這段代碼,直接返回 never
。
那正確的實現方式是什么啊?是這個:
type IsNever<T> = [T] extends [never] ? true : false
原理是什么啊?答曰:「通過放入 tuple 中,消除了聯合類型碰上 extends
時所產生的分配律」。
上面也提到了,在 typeScript 三元表達中,當 extends
前面的類型是聯合類型的時候,ts 就會產生類似于「乘法分配律」行為表現。具體可以用下面的示例來表述:
type Test = (AType | BType) extends SomeType ? 'yes' : 'no' = (AType extends SomeType ? 'yes' : 'no') | (BType extends SomeType ? 'yes' : 'no')
我們再來看看「乘法分配律」:(a+b)*c = a*c + b*c
。對比一下,我們就是知道,三元表達式中的 |
就是乘法分配律中的 +
, 三元表達式中的 extends
就是乘法分配律中的 *
。下面是表達這種類比的偽代碼:
type Test = (AType + BType) * (SomeType ? 'yes' : 'no') = AType * (SomeType ? 'yes' : 'no') + BType * (SomeType ? 'yes' : 'no')
另外,還有一個很重要的特性是,當聯合類型的泛型形參的出現在三元表達式中的真值或者假值分支語句中,它指代的是正在遍歷的聯合類型的成員元素。在編程世界里面,利用聯合類型的這個特性,我們可以遍歷聯合類型的所有成員類型。比如,ts 內置的 utility Exclude<T,U>
就是利用這種特性所實現的:
type MyExclude<T,U>= T extends U ? never : T; // 第二個條件分支語句中, T 指代的是正在遍歷的成員元素 type Test = MyExclude<'a'|'b'|'c', 'a'> // 'b'|'c'
在上面的實現中,在你將類型實參代入到三元表達式中,對于第二個條件分支的T
記得要理解為'a'|'b'|'c'
的各個成員元素,而不是理解為完整的聯合類型。
有時候,聯合類型的這種分配律不是我們想要的。那么,我們該怎么消除這種特性呢?其實上面在講「extends 與 never 」的時候也提到了。那就是,用方括號[]
包住 extends
前后的兩個類型參數。此時,兩個條件分支里面的聯合類型參數在實例化時候的值將會跟 extends
子句里面的是一樣的。
// 具有分配律的寫法 type ToArray<Type> = Type extends any ? Type[] : never; // type StrArrOrNumArr = ToArray<string | number>; // 結果是:`string[] | number[]` // 消除分配律的寫法 type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never; type StrArrOrNumArr2 = ToArray<string | number>; // 結果是:`(string | number)[]`
也許你會覺得 string[] | number[]
跟 (string | number)[]
是一樣的,我只能說:“客官,要不您再仔細瞧瞧?”。
在 typeScript 的類型編程世界里面,很多時候我們需要判斷兩個類型是否是一模一樣的,即這里所說的「嚴格相等」。如果讓你去實現這個 utility 的話,你會怎么做呢?我相信,不少人會跟我一樣,不假思索地寫下了下面的答案:
type IsEquals<T,U>= T extends U ? U extends T ? true : false : false
這個答案似乎是邏輯正確的。因為,如果只有自己才可能既是自己的子類型也是自己的父類型。然后,我們用很多測試用例去測,似乎結果也都符合我們的預期。直到我們碰到下面的邊緣用例:
type Test1= IsEquals<never,never> // 期待結果:true,實際結果: never type Test2= IsEquals<1,any> // 期待結果:false,實際結果: boolean type Test3= IsEquals<{readonly a: 1},{a:1}> // 期待結果:false,實際結果: true
沒辦法, typeScript 的類型系統有太多的違背常識的設計與實現了。如果還是沿用上面的思路,即使你把上面的特定用例修復好了,但是說不定還有其他的邊緣用例躲在某個陰暗的角度等著你。所以,對于「如何判斷兩個 typeScript 類型是嚴格相等」的這個問題上,目前社區里面從 typeScript 實現源碼角度上給出了一個終極答案:
type IsEquals<X, Y> = (<T>() => (T extends X ? 1 : 2)) extends (<T>() => (T extends Y ? 1 : 2)) ? true : false;
目前我還沒理解這個終極答案為什么是行之有效的,但是從測試結果來看,它確實是 work 的,并且被大家所公認。所以,目前為止,對于這個實現只能是死記硬背了。
type Test<A> = A extends SomeShape ? 第一個條件分支 : 第二支條件分支
當 typeScript 的三元表達式遇見類型推導infer SomeType
, 在語法上是有硬性要求的:
infer
只能出現在 extends
子句中,并且只能出現在 extends
關鍵字后面
緊跟在 infer
后面所聲明的類型形參只能在三元表達式的第一個條件分支(即,真值分支語句)中使用
除了語法上有硬性要求,我們也要正確理解 extends 遇見類型推導的語義。在這個上下文中,infer SomeType
更像是具有某種結構的類型的占位符。SomeShape
中可以通過 infer
來聲明多個類型形參,它們與一些已知的類型值共同組成了一個代表具有如此形態的SomeShape
。而 A extends SomeShape
是我們開發者在表達:「tsc,請按照顧我所聲明的這種結構去幫我推導得出各個泛型形參在運行時的值,以便供我進一步消費這些值」,而 tsc 會說:「好的,我盡我所能」。
「tsc 會盡我所能地去推導出具體的類型值」這句話的背后蘊含著不少的 typeScript 未在文檔上交代的行為表現。比如,當類型形參與類型值共同出現在「數組」,「字符串」等可遍歷的類型中,tsc 會產生類似于「子串/子數組匹配」的行為表現 - 也就是說,tsc 會以非貪婪匹配模式遍歷整個數組/字符串進行子串/數組匹配,直到匹配到最小的子串/子數組為止。這個結果,就是我們類型推導的泛型形參在運行時的值。
舉個例子,下面的代碼是實現一個ReplaceOnce
類型 utility 代碼:
type ReplaceOnce< S extends string, From extends string, To extends string > = From extends "" ? S : S extends `${infer Left}${From}${infer Right}` ? `${Left}${To}${Right}` : S “” type Test = Replace<"foobarbar", "bar", ""> // 結果是:“foobar”
tsc 在執行上面的這行代碼「S extends ${infer Left}${From}${infer Right}
」的時候,背后做了一個從左到右的「子串匹配」行為,直到匹配到所傳遞進來的子串From
為止。這個時候,也是 resolve 出形參Left
和Right
具體值的時候。
以上示例很好的表達出我想要表達的「當extends
跟類型推導結合到一塊所產生的一些微妙且未見諸于官方文檔的行為表現」。在 typeScript 高級類型編程中,善于利用這一點能夠幫助我們去解決很多「子串/子數組匹配」相關的問題。
讀到這里,這篇“typeScript的extends關鍵字怎么使用”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。