ホームページ >ウェブフロントエンド >jsチュートリアル >typeScript の extends キーワードについて話しましょう
extends
は、typeScript のキーワードです。 typeScript の型プログラミングの世界では、これが果たす役割は非常に重要なので、それに注目して深く学ぶ必要があります。私の意見では、これをマスターすることは、高度な TypeScript 型プログラミングの世界への足がかりになります。ただし、実際には、コンテキストや詳細が異なれば、意味論も大きく異なります。これを徹底的に整理しないと、開発者に大きな混乱を引き起こす可能性があります。それを徹底的に調べて学び、最終的にマスターする、これがこの記事を書く私の当初の目的です。
早速本題に入りましょう。typeScript では、さまざまなコンテキストで、extends
は次のセマンティクスを持ちます。セマンティクスが異なれば用途も異なります:
extends
は、Expression の interface
と組み合わせて使用できます。タイプの組み合わせ。
例 1-1
interface ChildComponentProps { onChange: (val: string)=> void } interface ParentComponentProps extends ChildComponentProps { value: string }
React コンポーネント開発モデルにはボトムアップ構築モデルがあり、すべての最下位レベルのコンポーネントを最初に配置する傾向があります。サブコンポーネントの props
が構築され、最後に コンテナ コンポーネント
(パブリック状態の促進、プロパティの集約と配布を担当) の props
が定義されます。現時点では、inferface の extends
は、このセマンティック要件、つまり型の組み合わせ (すべてのサブコンポーネントの props
を 1 つに集約したもの) を表現できます。
もちろん、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 }
これも合法である理由は、すべてが 1 つのコードから生じているためです。特徴: typeScript では、クラス変数は「値」と「型」の両方です。 interface extends class
のコンテキストでは、class が「型」セマンティクスであることは明らかです。インターフェイス extends
別のクラスは、このクラスのすべての実装コードを破棄し、このクラスの「型形状」とのみ組み合わせるインターフェイスとして理解できます。上記のコード例では、型の形状の観点から見ると、SomeClass
は次のインターフェイスと同等です:
例 1-4
interface SomeClass { name: string updateName: (name:string)=> void }
上記は、extends
キーワードの「型の組み合わせ」のセマンティクスです。事態は好転し始めた。
インターフェイス A がクラス B を継承する場合、このインターフェイス A は他のインターフェイスによって継承 (または結合) できます。ただし、クラスがこのインターフェイス A を implements
する必要がある場合、このクラスはクラス B 自体、またはクラス 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
クラスまたは # のサブクラスを継承する必要があります。 ##Control クラス:
例 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 キーワードを抽出します。 - の別のセマンティクス"継承"。 typeScript クラス間で
extends が使用される場合、その正確なセマンティクスは、オブジェクト指向 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 { }About
extends with "inheritance" semantics その他の動作機能この説明はすでにオブジェクト指向プログラミングのパラダイムの範疇に属しているため、ここでは詳しく説明しませんが、興味のある学生は自分で学習してください。
extends キーワードと
interface および
class の組み合わせによって表現される 2 つの異なるセマンティクスを理解しました。
#型の構成
更准确地说,这一节是要讨论 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; </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 上的运行结果。以上面这个心智模型(子集类型能赋值给父集类型,反之则不然)来看示例的运行结果,我们会有下面的直觉认知偏差:
any
は number
の親セットですが、これを number
型の値に割り当てることができるのはなぜですか? number | string
は number
の親セットである必要があるため、型 number
の値に割り当てることはできません。 。 number & string
が number
の親セットである必要があります。論理的に言えば、ここでエラーが報告されるべきですが、なぜ報告されないのでしょうか? {a:1}
は {a:1,b:2}
のサブセットです。論理的に言えば、# に割り当てることができます。 # #{a:1,b:2} 値を入力すると、エラーが報告されるのはなぜですか?
は
SubClass のサブセットであると感じます。論理的に言えば、
に代入できます。 SubClass 型の値、なぜエラーが報告されるのですか?
は
SubClass のサブセットであると感じます。論理的に言えば、
SubClass## 型の値に割り当てることができます。 #. なぜですか? エラーが報告されますか?
number
のサブセットを意味します。これは、
に代入できない理由です。
キーワード 。これは、
number|string extends numberの結果と等しいためです。明らかに、
(number string extends number オブジェクト型の型では # を使用できません。 ## サブタイプ メンタル モデルを理解するには、セット タイプは親セット タイプ = true
を拡張します。代わりに、親セット タイプはサブセット タイプ = true
を拡張します。サブセット タイプの明示的なリテラル キーと値のペアを使用する場合は、親セット タイプにも含める必要があります。そうでない場合、サブセット タイプに割り当てることはできません。
数値 & 文字列
は、オブジェクト タイプのタイプと見なす必要があります。上記のルールに従ってください。型制約を満たすための背後にあるルールを理解するには、「親タイプ」と「サブタイプ」の概念を使用する必要があります;
は宣言した型制約を満たすと言えます;型制約では、
AType## の場合、
AType は BTypeを拡張します。 # が
BTypeサブタイプの場合、
AType
次の「ts 型階層図」に従って 2 つの型を判断します。親子型関係:
注: 1)unknown
、null
は割り当てることができないため、typeScript 型システムの層になりません。他のタイプへ。
上の図に関しては、個別に強調表示できる点がいくつかあります:
anyどこにでも。これは、任意の型のサブタイプであり、任意の型のスーパータイプでもあり、任意の型自体である場合もあります。したがって、任意の型に割り当てることができます。
{} は、typeScript 型として使用される場合に特別な意味を持ちます。これは、
(Object.prototype.__proto__)= null に対応します。 は、js プロトタイプ チェーン上の位置にあり、すべてのオブジェクト型の基本クラスとみなされます。
のリテラル形式のサブタイプは tuple であり、
function### のリテラル形式のサブタイプは ### 関数式ですタイプ###。 ###tuple### および ###関数式型 ### は、###Literal 型 ### に含まれます。 #########この新しいメンタル モデルを使用して、###例 2-1### エラーが報告される場所: ###type Test1_1 = UselessType<number></number>
之所以报错,是因为在类型约束中,如果 extends
前面的类型是联合类型,那么要想满足类型约束,则联合类型的每一个成员都必须满足类型约束才行。这就是所谓的「联合类型的分配律」。显然,string extends number
是不成立的,所以整个联合类型就不满足类型约束;type Test2_1 = UselessType2
之所以报错,是因为{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
和 type Test3_2 = UselessType3<baseclass> </baseclass>
报错的原因也是因为因为缺少了相应的属性/方法,所以,它们都不是SubClass
的子类型。到这里,我们算是剖析完毕。下面总结一下。
extends
紧跟在泛型形参后面时,它是在表达「类型约束」的语义;AType extends BType
中,只有 AType
是 BType
的子类型,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 类型层级关系图」,我相信你已经知道答案了。如果你是基于「鸭子辩型」的直观理解去判断,那么我相信你的答案是true
。但是我的遗憾地告诉你,在 typeScript@4.9.4中,答案是false
。这明显是违背人类直觉的。于是乎,你会有这么一个疑问:“字面量类型 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></t>
就是利用这种特性所实现的:
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
有以下几个语义:
最值得注意的是,extends
在条件类型中与其他几个特殊类型结合所产生的特殊语义。几个特殊类型是:
{}
any
never
联合类型
【推荐学习:javascript高级教程】
以上がtypeScript の extends キーワードについて話しましょうの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。