红塔湾沙梭 發表於 2021-2-8 16:10:00

TypeScript 高级用法

<blockquote data-tool="mdnice编辑器">
<p>本文主要介绍 TypeScript 的高级用法,适用于对 TypeScript 已经有所了解或者已经实际用过一段时间的同学,分别从类型、运算符、操作符、泛型的角度来系统介绍常见的 TypeScript 文章没有好好讲解的功能点,最后再分享一下自己的实践经历。</p>
</blockquote>
<h2 data-tool="mdnice编辑器">一、 类型</h2>
<h4 data-tool="mdnice编辑器">unknown</h4>
<p data-tool="mdnice编辑器">unknown 指的是<strong>不可预先定义的类型</strong>,在很多场景下,它可以替代 any 的功能同时保留静态检查的能力。</p>
<pre data-tool="mdnice编辑器"><code>const&nbsp;num:&nbsp;number&nbsp;=&nbsp;10;<br>(num&nbsp;as&nbsp;unknown&nbsp;as&nbsp;string).split('');&nbsp;&nbsp;&nbsp;//&nbsp;注意,这里和any一样完全可以通过静态检查<br></code></pre>
<p data-tool="mdnice编辑器">这个时候 unknown 的作用就跟 any 高度类似了,你可以把它转化成任何类型,不同的地方是,在静态编译的时候,unknown 不能调用任何方法,而 any 可以。</p>
<pre data-tool="mdnice编辑器"><code>const&nbsp;foo:&nbsp;unknown&nbsp;=&nbsp;'string';<br>foo.substr(1);&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;Error:&nbsp;静态检查不通过报错<br>const&nbsp;bar:&nbsp;any&nbsp;=&nbsp;10;<br>any.substr(1);&nbsp;&nbsp;//&nbsp;Pass:&nbsp;any类型相当于放弃了静态检查<br></code></pre>
<p data-tool="mdnice编辑器">unknown 的一个使用场景是,避免使用 any 作为函数的参数类型而导致的静态类型检查 bug:</p>
<pre data-tool="mdnice编辑器"><code>function&nbsp;test(input:&nbsp;unknown):&nbsp;number&nbsp;{<br>&nbsp;&nbsp;if&nbsp;(Array.isArray(input))&nbsp;{<br>&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp;input.length;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;Pass:&nbsp;这个代码块中,类型守卫已经将input识别为array类型<br>&nbsp;&nbsp;}<br>&nbsp;&nbsp;return&nbsp;input.length;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// Error:&nbsp;这里的input还是unknown类型,静态检查报错。如果入参是any,则会放弃检查直接成功,带来报错风险<br>}<br></code></pre>
<h4 data-tool="mdnice编辑器">void</h4>
<p data-tool="mdnice编辑器">在 TS 中,void 和 undefined 功能高度类似,可以在逻辑上避免不小心使用了空指针导致的错误。</p>
<pre data-tool="mdnice编辑器"><code>function&nbsp;foo()&nbsp;{}&nbsp;&nbsp;&nbsp;//&nbsp;这个空函数没有返回任何值,返回类型缺省为void<br>const&nbsp;a&nbsp;=&nbsp;foo();&nbsp;//&nbsp;此时a的类型定义为void,你也不能调用a的任何属性方法<br></code></pre>
<p data-tool="mdnice编辑器">void 和 undefined 类型最大的区别是,你可以理解为 undefined 是 void 的一个子集,当你对函数返回值并不在意时,使用 void 而不是 undefined。举一个 React 中的实际的例子。</p>
<pre data-tool="mdnice编辑器"><code>//&nbsp;Parent.tsx<br>function&nbsp;Parent():&nbsp;JSX.Element&nbsp;{<br>&nbsp;&nbsp;const&nbsp;getValue&nbsp;=&nbsp;():&nbsp;number&nbsp;=&gt;&nbsp;{&nbsp;return&nbsp;2&nbsp;};&nbsp;&nbsp;&nbsp;&nbsp;/*&nbsp;这里函数返回的是number类型&nbsp;*/<br>&nbsp;&nbsp;//&nbsp;const&nbsp;getValue&nbsp;=&nbsp;():&nbsp;string&nbsp;=&gt;&nbsp;{&nbsp;return&nbsp;'str'&nbsp;};&nbsp;/*&nbsp;这里函数返回的string类型,同样可以传给子属性&nbsp;*/<br>&nbsp;&nbsp;return&nbsp;&lt;Child&nbsp;getValue={getValue}&nbsp;/&gt;<br>}<br></code></pre>
<pre data-tool="mdnice编辑器"><code>//&nbsp;Child.tsx<br>type&nbsp;Props&nbsp;=&nbsp;{<br>&nbsp;&nbsp;getValue:&nbsp;()&nbsp;=&gt;&nbsp;void;&nbsp;&nbsp;//&nbsp;这里的void表示逻辑上不关注具体的返回值类型,number、string、undefined等都可以<br>}<br>function&nbsp;Child({&nbsp;getValue&nbsp;}:&nbsp;Props)&nbsp;=&gt;&nbsp;&lt;div&gt;{getValue()}&lt;/div&gt;<br><br></code></pre>
<h4 data-tool="mdnice编辑器">never</h4>
<p data-tool="mdnice编辑器">never 是指没法正常结束返回的类型,一个必定会报错或者死循环的函数会返回这样的类型。</p>
<pre data-tool="mdnice编辑器"><code>function&nbsp;foo():&nbsp;never&nbsp;{&nbsp;throw&nbsp;new&nbsp;Error('error&nbsp;message')&nbsp;}&nbsp;&nbsp;//&nbsp;throw&nbsp;error&nbsp;返回值是never<br>function&nbsp;foo():&nbsp;never&nbsp;{&nbsp;while(true){}&nbsp;}&nbsp;&nbsp;//&nbsp;这个死循环的也会无法正常退出<br>function&nbsp;foo():&nbsp;never&nbsp;{&nbsp;let&nbsp;count&nbsp;=&nbsp;1;&nbsp;while(count){&nbsp;count&nbsp;++;&nbsp;}&nbsp;}&nbsp;&nbsp;//&nbsp;Error:&nbsp;这个无法将返回值定义为never,因为无法在静态编译阶段直接识别出<br></code></pre>
<p data-tool="mdnice编辑器">还有就是永远没有相交的类型。</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;human&nbsp;=&nbsp;'boy'&nbsp;&amp;&nbsp;'girl'&nbsp;//&nbsp;这两个单独的字符串类型并不可能相交,故human为never类型<br></code></pre>
<p data-tool="mdnice编辑器">不过任何类型联合上 never 类型,还是原来的类型。</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;language&nbsp;=&nbsp;'ts'&nbsp;|&nbsp;never&nbsp;&nbsp;&nbsp;//&nbsp;language的类型还是'ts'类型<br></code></pre>
<p data-tool="mdnice编辑器">关于 never 有如下特性:</p>
<ul class="list-paddingleft-2" data-tool="mdnice编辑器">
<li>在一个函数中调用了返回 never 的函数后,之后的代码都会变成<code>deadcode</code></li>
</ul>
<pre data-tool="mdnice编辑器"><code>function&nbsp;test()&nbsp;{<br>&nbsp;&nbsp;foo();&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;这里的foo指上面返回never的函数<br>&nbsp;&nbsp;console.log(111);&nbsp;&nbsp;//&nbsp;Error:&nbsp;编译器报错,此行代码永远不会执行到<br>}<br></code></pre>
<ul class="list-paddingleft-2" data-tool="mdnice编辑器">
<li>无法把其他类型赋给 never。</li>
</ul>
<pre data-tool="mdnice编辑器"><code>let&nbsp;n:&nbsp;never;<br>let&nbsp;o:&nbsp;any&nbsp;=&nbsp;{};<br>n&nbsp;=&nbsp;o;&nbsp;&nbsp;//&nbsp;Error:&nbsp;不能把一个非never类型赋值给never类型,包括any<br></code></pre>
<p data-tool="mdnice编辑器">关于 never 的这个特性有一些很 hack 的用法和讨论,比如这个知乎下的尤雨溪的回答:https://www.zhihu.com/question/354601204/answer/888551021。</p>
<h2 data-tool="mdnice编辑器">二、运算符</h2>
<h4 data-tool="mdnice编辑器">非空断言运算符 !</h4>
<p data-tool="mdnice编辑器">这个运算符可以用在变量名或者函数名之后,用来强调对应的元素是非 null|undefined 的。</p>
<pre data-tool="mdnice编辑器"><code>function&nbsp;onClick(callback?:&nbsp;()&nbsp;=&gt;&nbsp;void)&nbsp;{<br>&nbsp;&nbsp;callback!();&nbsp;&nbsp;//&nbsp;参数是可选入参,加了这个感叹号!之后,TS编译不报错<br>}<br></code></pre>
<p data-tool="mdnice编辑器">你可以查看编译后的 ES5 代码,居然没有做任何防空判断。</p>
<pre data-tool="mdnice编辑器"><code>function&nbsp;onClick(callback)&nbsp;{<br>&nbsp;&nbsp;callback();<br>}<br></code></pre>
<p data-tool="mdnice编辑器">这个符号的场景,特别适用于我们已经明确知道不会返回空值的场景,从而减少冗余的代码判断,如 React 的 Ref。</p>
<pre data-tool="mdnice编辑器"><code>function&nbsp;Demo():&nbsp;JSX.Elememt&nbsp;{<br>&nbsp;&nbsp;const&nbsp;divRef&nbsp;=&nbsp;useRef&lt;HTMLDivElement&gt;();<br>&nbsp;&nbsp;useEffect(()&nbsp;=&gt;&nbsp;{<br>&nbsp;&nbsp;&nbsp;&nbsp;divRef.current!.scrollIntoView();&nbsp;&nbsp;//&nbsp;当组件Mount后才会触发useEffect,故current一定是有值的<br>&nbsp;&nbsp;},&nbsp;[]);<br>&nbsp;&nbsp;return&nbsp;&lt;div&nbsp;ref={divRef}&gt;Demo&lt;/div&gt;<br>}<br></code></pre>
<h4 data-tool="mdnice编辑器">可选链运算符 ?.</h4>
<p data-tool="mdnice编辑器">相比上面!作用于编译阶段的非空判断,<code>?.</code>这个是开发者最需要的运行时(当然编译时也有效)的非空判断。</p>
<pre data-tool="mdnice编辑器"><code>obj?.prop&nbsp;&nbsp;&nbsp;&nbsp;obj?.&nbsp;&nbsp;&nbsp;&nbsp;func?.(args)<br></code></pre>
<p data-tool="mdnice编辑器">?.用来判断左侧的表达式是否是 null | undefined,如果是则会停止表达式运行,可以减少我们大量的&amp;&amp;运算。</p>
<p data-tool="mdnice编辑器">比如我们写出<code>a?.b</code>时,编译器会自动生成如下代码</p>
<pre data-tool="mdnice编辑器"><code>a&nbsp;===&nbsp;null&nbsp;||&nbsp;a&nbsp;===&nbsp;void&nbsp;0&nbsp;?&nbsp;void&nbsp;0&nbsp;:&nbsp;a.b;<br></code></pre>
<p data-tool="mdnice编辑器">这里涉及到一个小知识点:<code>undefined</code>这个值在非严格模式下会被重新赋值,使用<code>void 0</code>必定返回真正的 undefined。</p>
<h4 data-tool="mdnice编辑器">空值合并运算符 ??</h4>
<p data-tool="mdnice编辑器">??与||的功能是相似的,区别在于<strong>??在左侧表达式结果为 null 或者 undefined 时,才会返回右侧表达式。</strong></p>
<p data-tool="mdnice编辑器">比如我们书写了<code>let b = a ?? 10</code>,生成的代码如下:</p>
<pre data-tool="mdnice编辑器"><code>let&nbsp;b&nbsp;=&nbsp;a&nbsp;!==&nbsp;null&nbsp;&amp;&amp;&nbsp;a&nbsp;!==&nbsp;void&nbsp;0&nbsp;?&nbsp;a&nbsp;:&nbsp;10;<br></code></pre>
<p data-tool="mdnice编辑器">而 || 表达式,大家知道的,则对 false、''、NaN、0 等逻辑空值也会生效,不适于我们做对参数的合并。</p>
<h4 data-tool="mdnice编辑器">数字分隔符_</h4>
<pre data-tool="mdnice编辑器"><code>let&nbsp;num:number&nbsp;=&nbsp;1_2_345.6_78_9<br></code></pre>
<p data-tool="mdnice编辑器">_可以用来对长数字做任意的分隔,主要设计是为了便于数字的阅读,编译出来的代码是没有下划线的,请放心食用。</p>
<h2 data-tool="mdnice编辑器">三、操作符</h2>
<h4 data-tool="mdnice编辑器">键值获取 keyof</h4>
<p data-tool="mdnice编辑器">keyof 可以获取一个类型所有键值,返回一个联合类型,如下:</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;Person&nbsp;=&nbsp;{<br>&nbsp;&nbsp;name:&nbsp;string;<br>&nbsp;&nbsp;age:&nbsp;number;<br>}<br>type&nbsp;PersonKey&nbsp;=&nbsp;keyof&nbsp;Person;&nbsp;&nbsp;//&nbsp;PersonKey得到的类型为&nbsp;'name'&nbsp;|&nbsp;'age'<br></code></pre>
<p data-tool="mdnice编辑器">keyof 的一个典型用途是限制访问对象的 key 合法化,因为 any 做索引是不被接受的。</p>
<pre data-tool="mdnice编辑器"><code>function&nbsp;getValue&nbsp;(p:&nbsp;Person,&nbsp;k:&nbsp;keyof&nbsp;Person)&nbsp;{<br>&nbsp;&nbsp;return&nbsp;p;&nbsp;&nbsp;//&nbsp;如果k不如此定义,则无法以p的代码格式通过编译<br>}<br></code></pre>
<p data-tool="mdnice编辑器">总结起来 keyof 的语法格式如下:</p>
<pre data-tool="mdnice编辑器"><code>类型&nbsp;=&nbsp;keyof&nbsp;类型<br></code></pre>
<h4 data-tool="mdnice编辑器">实例类型获取 typeof</h4>
<p data-tool="mdnice编辑器">typeof 是获取一个对象/实例的类型,如下:</p>
<pre data-tool="mdnice编辑器"><code>const&nbsp;me:&nbsp;Person&nbsp;=&nbsp;{&nbsp;name:&nbsp;'gzx',&nbsp;age:&nbsp;16&nbsp;};<br>type&nbsp;P&nbsp;=&nbsp;typeof&nbsp;me;&nbsp;&nbsp;//&nbsp;{&nbsp;name:&nbsp;string,&nbsp;age:&nbsp;number&nbsp;|&nbsp;undefined&nbsp;}<br>const&nbsp;you:&nbsp;typeof&nbsp;me&nbsp;=&nbsp;{&nbsp;name:&nbsp;'mabaoguo',&nbsp;age:&nbsp;69&nbsp;}&nbsp;&nbsp;//&nbsp;可以通过编译<br></code></pre>
<p data-tool="mdnice编辑器">typeof 只能用在具体的对象上,这与 js 中的 typeof 是一致的,并且它会根据左侧值自动决定应该执行哪种行为。</p>
<pre data-tool="mdnice编辑器"><code>const&nbsp;typestr&nbsp;=&nbsp;typeof&nbsp;me;&nbsp;&nbsp;&nbsp;//&nbsp;typestr的值为"object"<br></code></pre>
<p data-tool="mdnice编辑器">typeof 可以和 keyof 一起使用(因为 typeof 是返回一个类型嘛),如下:</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;PersonKey&nbsp;=&nbsp;keyof&nbsp;typeof&nbsp;me;&nbsp;&nbsp;&nbsp;//&nbsp;'name'&nbsp;|&nbsp;'age'<br></code></pre>
<p data-tool="mdnice编辑器">总结起来 typeof 的语法格式如下:</p>
<pre data-tool="mdnice编辑器"><code>类型&nbsp;=&nbsp;typeof&nbsp;实例对象<br></code></pre>
<h4 data-tool="mdnice编辑器">遍历属性 in</h4>
<p data-tool="mdnice编辑器">in 只能用在类型的定义中,可以对枚举类型进行遍历,如下:</p>
<pre data-tool="mdnice编辑器"><code>//&nbsp;这个类型可以将任何类型的键值转化成number类型<br>type&nbsp;TypeToNumber&lt;T&gt;&nbsp;=&nbsp;{<br>&nbsp;&nbsp;:&nbsp;number<br>}<br></code></pre>
<p data-tool="mdnice编辑器"><code>keyof</code>返回泛型 T 的所有键枚举类型,<code>key</code>是自定义的任何变量名,中间用<code>in</code>链接,外围用<code>[]</code>包裹起来(这个是固定搭配),冒号右侧<code>number</code>将所有的<code>key</code>定义为<code>number</code>类型。</p>
<p data-tool="mdnice编辑器">于是可以这样使用了:</p>
<pre data-tool="mdnice编辑器"><code>const&nbsp;obj:&nbsp;TypeToNumber&lt;Person&gt;&nbsp;=&nbsp;{&nbsp;name:&nbsp;10,&nbsp;age:&nbsp;10&nbsp;}<br></code></pre>
<p data-tool="mdnice编辑器">总结起来 in 的语法格式如下:</p>
<pre data-tool="mdnice编辑器"><code>[&nbsp;自定义变量名&nbsp;in&nbsp;枚举类型&nbsp;]:&nbsp;类型<br></code></pre>
<h2 data-tool="mdnice编辑器">四、泛型</h2>
<p data-tool="mdnice编辑器">泛型在 TS 中可以说是一个非常重要的属性,它承载了从静态定义到动态调用的桥梁,同时也是 TS 对自己类型定义的元编程。泛型可以说是 TS 类型工具的精髓所在,也是整个 TS 最难学习的部分,这里专门分两章总结一下。</p>
<h4 data-tool="mdnice编辑器">基本使用</h4>
<p data-tool="mdnice编辑器">泛型可以用在普通类型定义,类定义、函数定义上,如下:</p>
<pre data-tool="mdnice编辑器"><code>//&nbsp;普通类型定义<br>type&nbsp;Dog&lt;T&gt;&nbsp;=&nbsp;{&nbsp;name:&nbsp;string,&nbsp;type:&nbsp;T&nbsp;}<br>//&nbsp;普通类型使用<br>const&nbsp;dog:&nbsp;Dog&lt;number&gt;&nbsp;=&nbsp;{&nbsp;name:&nbsp;'ww',&nbsp;type:&nbsp;20&nbsp;}<br><br>//&nbsp;类定义<br>class&nbsp;Cat&lt;T&gt;&nbsp;{<br>&nbsp;&nbsp;private&nbsp;type:&nbsp;T;<br>&nbsp;&nbsp;constructor(type:&nbsp;T)&nbsp;{&nbsp;this.type&nbsp;=&nbsp;type;&nbsp;}<br>}<br>//&nbsp;类使用<br>const&nbsp;cat:&nbsp;Cat&lt;number&gt;&nbsp;=&nbsp;new&nbsp;Cat&lt;number&gt;(20);&nbsp;//&nbsp;或简写&nbsp;const&nbsp;cat&nbsp;=&nbsp;new&nbsp;Cat(20)<br><br>//&nbsp;函数定义<br>function&nbsp;swipe&lt;T,&nbsp;U&gt;(value:&nbsp;):&nbsp;&nbsp;{<br>&nbsp;&nbsp;return&nbsp;,&nbsp;value];<br>}<br>//&nbsp;函数使用<br>swipe&lt;Cat&lt;number&gt;,&nbsp;Dog&lt;number&gt;&gt;()&nbsp;&nbsp;//&nbsp;或简写&nbsp;swipe()<br></code></pre>
<p data-tool="mdnice编辑器">注意,如果对一个类型名定义了泛型,那么使用此类型名的时候一定要把泛型类型也写上去。</p>
<p data-tool="mdnice编辑器">而对于变量来说,它的类型可以在调用时推断出来的话,就可以省略泛型书写。</p>
<p data-tool="mdnice编辑器">泛型的语法格式简单总结如下:</p>
<pre data-tool="mdnice编辑器"><code>类型名&lt;泛型列表&gt;&nbsp;具体类型定义<br></code></pre>
<h4 data-tool="mdnice编辑器">泛型推导与默认值</h4>
<p>上面提到了,我们可以简化对泛型类型定义的书写,因为TS会自动根据变量定义时的类型推导出变量类型,这一般是发生在函数调用的场合的。</p>
<pre><code>type&nbsp;Dog&lt;T&gt;&nbsp;=&nbsp;{&nbsp;name:&nbsp;string,&nbsp;type:&nbsp;T&nbsp;}<br><br>function&nbsp;adopt&lt;T&gt;(dog:&nbsp;Dog&lt;T&gt;)&nbsp;{&nbsp;return&nbsp;dog&nbsp;};<br><br>const&nbsp;dog&nbsp;=&nbsp;{&nbsp;name:&nbsp;'ww',&nbsp;type:&nbsp;'hsq'&nbsp;};&nbsp;&nbsp;//&nbsp;这里按照Dog类型的定义一个type为string的对象<br>adopt(dog);&nbsp;&nbsp;//&nbsp;Pass:&nbsp;函数会根据入参类型推断出type为string<br></code></pre>
<p>若不适用函数泛型推导,我们若需要定义变量类型则必须指定泛型类型。</p>
<pre><code>const&nbsp;dog:&nbsp;Dog&lt;string&gt;&nbsp;=&nbsp;{&nbsp;name:&nbsp;'ww',&nbsp;type:&nbsp;'hsq'&nbsp;}&nbsp;&nbsp;//&nbsp;不可省略&lt;string&gt;这部分<br></code></pre>
<p>如果我们想不指定,可以使用泛型默认值的方案。</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;Dog&lt;T&nbsp;=&nbsp;any&gt;&nbsp;=&nbsp;{&nbsp;name:&nbsp;string,&nbsp;type:&nbsp;T&nbsp;}<br>const&nbsp;dog:&nbsp;Dog&nbsp;=&nbsp;{&nbsp;name:&nbsp;'ww',&nbsp;type:&nbsp;'hsq'&nbsp;}<br>dog.type&nbsp;=&nbsp;123;&nbsp;&nbsp;&nbsp;&nbsp;//&nbsp;不过这样type类型就是any了,无法自动推导出来,失去了泛型的意义<br></code></pre>
<p data-tool="mdnice编辑器">泛型默认值的语法格式简单总结如下:</p>
<pre data-tool="mdnice编辑器"><code>泛型名&nbsp;=&nbsp;默认类型<br></code></pre>
<h4 data-tool="mdnice编辑器">泛型约束</h4>
<p data-tool="mdnice编辑器">有的时候,我们可以不用关注泛型具体的类型,如:</p>
<pre data-tool="mdnice编辑器"><code>function&nbsp;fill&lt;T&gt;(length:&nbsp;number,&nbsp;value:&nbsp;T):&nbsp;T[]&nbsp;{<br>&nbsp;&nbsp;return&nbsp;new&nbsp;Array(length).fill(value);<br>}<br></code></pre>
<p data-tool="mdnice编辑器">这个函数接受一个长度参数和默认值,结果就是生成使用默认值填充好对应个数的数组。我们不用对传入的参数做判断,直接填充就行了,但是有时候,我们需要限定类型,这时候使用<code>extends</code>关键字即可。</p>
<pre data-tool="mdnice编辑器"><code>function&nbsp;sum&lt;T&nbsp;extends&nbsp;number&gt;(value:&nbsp;T[]):&nbsp;number&nbsp;{<br>&nbsp;&nbsp;let&nbsp;count&nbsp;=&nbsp;0;<br>&nbsp;&nbsp;value.forEach(v&nbsp;=&gt;&nbsp;count&nbsp;+=&nbsp;v);<br>&nbsp;&nbsp;return&nbsp;count;<br>}<br></code></pre>
<p data-tool="mdnice编辑器">这样你就可以以<code>sum()</code>这种方式调用求和函数,而像<code>sum(['1', '2'])</code>这种是无法通过编译的。</p>
<p data-tool="mdnice编辑器">泛型约束也可以用在多个泛型参数的情况</p>
<pre data-tool="mdnice编辑器"><code>function&nbsp;pick&lt;T,&nbsp;U&nbsp;extends&nbsp;keyof&nbsp;T&gt;(){};<br></code></pre>
<p data-tool="mdnice编辑器">这里的意思是限制了 U 一定是 T 的 key 类型中的子集,这种用法常常出现在一些泛型工具库中。</p>
<p data-tool="mdnice编辑器">extends 的语法格式简单总结如下,注意下面的类型既可以是一般意义上的类型也可以是泛型。</p>
<pre data-tool="mdnice编辑器"><code>泛型名&nbsp;extends&nbsp;类型<br></code></pre>
<h4 data-tool="mdnice编辑器">泛型条件</h4>
<p data-tool="mdnice编辑器">上面提到 extends,其实也可以当做一个三元运算符,如下:</p>
<pre data-tool="mdnice编辑器"><code>T&nbsp;extends&nbsp;U?&nbsp;X:&nbsp;Y<br></code></pre>
<p data-tool="mdnice编辑器">这里便不限制 T 一定要是 U 的子类型,如果是 U 子类型,则将 T 定义为 X 类型,否则定义为 Y 类型。</p>
<p data-tool="mdnice编辑器">注意,生成的结果是<strong>分配式的</strong>。</p>
<p data-tool="mdnice编辑器">举个例子,如果我们把 X 换成 T,如此形式:<code>T extends U? T: never</code></p>
<p data-tool="mdnice编辑器">此时返回的 T,是满足原来的 T 中包含 U 的部分,可以理解为 T 和 U 的<strong>交集。</strong></p>
<p data-tool="mdnice编辑器">所以,extends 的语法格式可以扩展为:</p>
<pre data-tool="mdnice编辑器"><code>泛型名A&nbsp;extends&nbsp;类型B&nbsp;?&nbsp;类型C:&nbsp;类型D<br></code></pre>
<h4 data-tool="mdnice编辑器">泛型推断 infer</h4>
<p data-tool="mdnice编辑器">infer 的中文是“推断”的意思,一般是搭配上面的泛型条件语句使用的,所谓推断,就是你不用预先指定在泛型列表中,在运行时会自动判断,不过你得先预定义好整体的结构。举个例子:</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;Foo&lt;T&gt;&nbsp;=&nbsp;T&nbsp;extends&nbsp;{t:&nbsp;infer&nbsp;Test}&nbsp;?&nbsp;Test:&nbsp;string<br></code></pre>
<p data-tool="mdnice编辑器">首选看 extends 后面的内容,<code>{t: infer Test}</code>可以看成是一个包含<code>t属性</code>的<strong>类型定义</strong>,这个<code>t属性</code>的 value 类型通过<code>infer</code>进行推断后会赋值给<code>Test</code>类型,如果泛型实际参数符合<code>{t: infer Test}</code>的定义那么返回的就是<code>Test</code>类型,否则默认给缺省的<code>string</code>类型。举个例子加深下理解:</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;One&nbsp;=&nbsp;Foo&lt;number&gt;&nbsp;&nbsp;//&nbsp;string,因为number不是一个包含t的对象类型<br>type&nbsp;Two&nbsp;=&nbsp;Foo&lt;{t:&nbsp;boolean}&gt;&nbsp;&nbsp;//&nbsp;boolean,因为泛型参数匹配上了,使用了infer对应的type<br>type&nbsp;Three&nbsp;=&nbsp;Foo&lt;{a:&nbsp;number,&nbsp;t:&nbsp;()&nbsp;=&gt;&nbsp;void}&gt;&nbsp;//&nbsp;()&nbsp;=&gt;&nbsp;void,泛型定义是参数的子集,同样适配<br></code></pre>
<p data-tool="mdnice编辑器"><code>infer</code>用来对满足的泛型类型进行子类型的抽取,有很多高级的泛型工具也巧妙的使用了这个方法。</p>
<h2 data-tool="mdnice编辑器">五、泛型工具</h2>
<h4 data-tool="mdnice编辑器">Partical&lt;T&gt;</h4>
<p data-tool="mdnice编辑器">此工具的作用就是将泛型中全部属性变为可选的:</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;Partial&lt;T&gt;&nbsp;=&nbsp;{<br>&nbsp;?:&nbsp;T<br>}<br></code></pre>
<p data-tool="mdnice编辑器">举个例子,这个类型定义在下面也会用到:</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;Animal&nbsp;=&nbsp;{<br>&nbsp;&nbsp;name:&nbsp;string,<br>&nbsp;&nbsp;category:&nbsp;string,<br>&nbsp;&nbsp;age:&nbsp;number,<br>&nbsp;&nbsp;eat:&nbsp;()&nbsp;=&gt;&nbsp;number<br>}<br></code></pre>
<p data-tool="mdnice编辑器">使用 Partical 包裹一下:</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;PartOfAnimal&nbsp;=&nbsp;Partical&lt;Animal&gt;;<br>const&nbsp;ww:&nbsp;PartOfAnimal&nbsp;=&nbsp;{&nbsp;name:&nbsp;'ww'&nbsp;};&nbsp;//&nbsp;属性全部可选后,可以只赋值部分属性了<br></code></pre>
<h4 data-tool="mdnice编辑器">Record&lt;K, T&gt;</h4>
<p data-tool="mdnice编辑器">此工具的作用是将 K 中所有属性值转化为 T 类型,我们常用它来申明一个普通 object 对象。</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;Record&lt;K&nbsp;extends&nbsp;keyof&nbsp;any,T&gt;&nbsp;=&nbsp;{<br>&nbsp;&nbsp;:&nbsp;T<br>}<br></code></pre>
<p data-tool="mdnice编辑器">这里特别说明一下,<code>keyof any</code>对应的类型为<code>number | string | symbol</code>,也就是可以做对象键(专业说法叫索引 index)的类型集合。</p>
<p data-tool="mdnice编辑器">举个例子:</p>
<pre data-tool="mdnice编辑器"><code>const&nbsp;obj:&nbsp;Record&lt;string,&nbsp;string&gt;&nbsp;=&nbsp;{&nbsp;'name':&nbsp;'mbg',&nbsp;'tag':&nbsp;'年轻人不讲武德'&nbsp;}<br></code></pre>
<h4 data-tool="mdnice编辑器">Pick&lt;T, K&gt;</h4>
<p data-tool="mdnice编辑器">此工具的作用是将 T 类型中的 K 键列表提取出来,生成新的子键值对类型。</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;Pick&lt;T,&nbsp;K&nbsp;extends&nbsp;keyof&nbsp;T&gt;&nbsp;=&nbsp;{<br>&nbsp;&nbsp;:&nbsp;T<br>}<br></code></pre>
<p data-tool="mdnice编辑器">我们还是用上面的<code>Animal</code>定义,看一下 Pick 如何使用。</p>
<pre data-tool="mdnice编辑器"><code>const&nbsp;bird:&nbsp;Pick&lt;Animal,&nbsp;"name"&nbsp;|&nbsp;"age"&gt;&nbsp;=&nbsp;{&nbsp;name:&nbsp;'bird',&nbsp;age:&nbsp;1&nbsp;}<br></code></pre>
<h4 data-tool="mdnice编辑器">Exclude&lt;T, U&gt;</h4>
<p data-tool="mdnice编辑器">此工具是在 T 类型中,去除 T 类型和 U 类型的交集,返回剩余的部分。</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;Exclude&lt;T,&nbsp;U&gt;&nbsp;=&nbsp;T&nbsp;extends&nbsp;U&nbsp;?&nbsp;never&nbsp;:&nbsp;T<br></code></pre>
<p data-tool="mdnice编辑器">注意这里的 extends 返回的 T 是原来的 T 中和 U 无交集的属性,而任何属性联合 never 都是自身,具体可在上文查阅。</p>
<p data-tool="mdnice编辑器">举个例子:</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;T1&nbsp;=&nbsp;Exclude&lt;"a"&nbsp;|&nbsp;"b"&nbsp;|&nbsp;"c",&nbsp;"a"&nbsp;|&nbsp;"b"&gt;;&nbsp;&nbsp;&nbsp;//&nbsp;"c"<br>type&nbsp;T2&nbsp;=&nbsp;Exclude&lt;string&nbsp;|&nbsp;number&nbsp;|&nbsp;(()&nbsp;=&gt;&nbsp;void),&nbsp;Function&gt;;&nbsp;//&nbsp;string&nbsp;|&nbsp;number<br></code></pre>
<h4 data-tool="mdnice编辑器">Omit&lt;T, K&gt;</h4>
<p data-tool="mdnice编辑器">此工具可认为是适用于键值对对象的 Exclude,它会去除类型 T 中包含 K 的键值对。</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;Omit&nbsp;=&nbsp;Pick&lt;T,&nbsp;Exclude&lt;keyof&nbsp;T,&nbsp;K&gt;&gt;<br></code></pre>
<p data-tool="mdnice编辑器">在定义中,第一步先从 T 的 key 中去掉与 K 重叠的 key,接着使用 Pick 把 T 类型和剩余的 key 组合起来即可。</p>
<p data-tool="mdnice编辑器">还是用上面的 Animal 举个例子</p>
<pre data-tool="mdnice编辑器"><code>const&nbsp;OmitAnimal:Omit&lt;Animal,&nbsp;'name'|'age'&gt;&nbsp;=&nbsp;{&nbsp;category:&nbsp;'lion',&nbsp;eat:&nbsp;()&nbsp;=&gt;&nbsp;{&nbsp;console.log('eat')&nbsp;}&nbsp;}<br></code></pre>
<p data-tool="mdnice编辑器">可以发现,Omit 与 Pick 得到的结果完全相反,一个是取非结果,一个取交结果。</p>
<h4 data-tool="mdnice编辑器">ReturnType&lt;T&gt;</h4>
<p data-tool="mdnice编辑器">此工具就是获取 T 类型(函数)对应的返回值类型。</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;ReturnType&lt;T&nbsp;extends&nbsp;(...args:&nbsp;any)&nbsp;=&gt;&nbsp;any&gt;<br>&nbsp;&nbsp;=&nbsp;T&nbsp;extends&nbsp;(...args:&nbsp;any)&nbsp;=&gt;&nbsp;infer&nbsp;R&nbsp;?&nbsp;R&nbsp;:&nbsp;any;<br></code></pre>
<p data-tool="mdnice编辑器">看源码其实有点多,其实可以稍微简化成下面的样子。</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;ReturnType&lt;T&nbsp;extends&nbsp;func&gt;&nbsp;=&nbsp;T&nbsp;extends&nbsp;()&nbsp;=&gt;&nbsp;infer&nbsp;R&nbsp;?&nbsp;R:&nbsp;any;<br></code></pre>
<p data-tool="mdnice编辑器">通过使用 infer 推断返回值类型,然后返回此类型,如果你彻底理解了 infer 的含义,那这段就很好理解。</p>
<p data-tool="mdnice编辑器">举个例子:</p>
<pre data-tool="mdnice编辑器"><code>function&nbsp;foo(x:&nbsp;string&nbsp;|&nbsp;number):&nbsp;string&nbsp;|&nbsp;number&nbsp;{&nbsp;/*..*/&nbsp;}<br>type&nbsp;FooType&nbsp;=&nbsp;ReturnType&lt;foo&gt;;&nbsp;&nbsp;//&nbsp;string&nbsp;|&nbsp;number<br></code></pre>
<h4 data-tool="mdnice编辑器">Required&lt;T&gt;</h4>
<p data-tool="mdnice编辑器">此工具可以将类型 T 中所有的属性变为必选项。</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;Required&lt;T&gt;&nbsp;=&nbsp;{<br>&nbsp;&nbsp;-?:&nbsp;T<br>}<br></code></pre>
<p data-tool="mdnice编辑器">这里有一个很有意思的语法<code>-?</code>,你可以理解为就是 TS 中把?可选属性减去的意思。</p>
<p data-tool="mdnice编辑器">除了这些以外,还有很多的内置的类型工具,可以参考TypeScript Handbook<sup></sup>获得更详细的信息,同时 Github 上也有很多第三方类型辅助工具,如utility-types<sup></sup>等。</p>
<h2 data-tool="mdnice编辑器">六、项目实战</h2>
<p data-tool="mdnice编辑器">这里分享一些我个人的想法,可能也许会比较片面甚至错误,欢迎大家积极留言讨论。</p>
<h4 data-tool="mdnice编辑器">Q: 偏好使用 interface 还是 type 来定义类型?</h4>
<p data-tool="mdnice编辑器">A: 从用法上来说两者本质上没有区别,大家使用 React 项目做业务开发的话,主要就是用来定义 Props 以及接口数据类型。</p>
<p data-tool="mdnice编辑器">但是从扩展的角度来说,type 比 interface 更方便拓展一些,假如有以下两个定义:</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;Name&nbsp;=&nbsp;{&nbsp;name:&nbsp;string&nbsp;};<br>interface&nbsp;IName&nbsp;{&nbsp;name:&nbsp;string&nbsp;};<br></code></pre>
<p data-tool="mdnice编辑器">想要做类型的扩展的话,type 只需要一个<code>&amp;</code>,而 interface 要多写不少代码。</p>
<pre data-tool="mdnice编辑器"><code>type&nbsp;Person&nbsp;=&nbsp;Name&nbsp;&amp;&nbsp;{&nbsp;age:&nbsp;number&nbsp;};<br>interface&nbsp;IPerson&nbsp;extends&nbsp;IName&nbsp;{&nbsp;age:&nbsp;number&nbsp;};<br></code></pre>
<p data-tool="mdnice编辑器">另外 type 有一些 interface 做不到的事情,比如使用<code>|</code>进行枚举类型的组合,使用<code>typeof</code>获取定义的类型等等。</p>
<p data-tool="mdnice编辑器">不过 interface 有一个比较强大的地方就是可以重复定义添加属性,比如我们需要给<code>window</code>对象添加一个自定义的属性或者方法,那么我们直接基于其 Interface 新增属性就可以了。</p>
<pre data-tool="mdnice编辑器"><code>declare&nbsp;global&nbsp;{<br>&nbsp;&nbsp;&nbsp;&nbsp;interface&nbsp;Window&nbsp;{&nbsp;MyNamespace:&nbsp;any;&nbsp;}<br>}<br></code></pre>
<p data-tool="mdnice编辑器">总体来说,大家知道 TS 是类型兼容而不是类型名称匹配的,所以一般不需用面向对象的场景或者不需要修改全局类型的场合,我一般都是用 type 来定义类型。</p>
<h4 data-tool="mdnice编辑器">Q: 是否允许 any 类型的出现</h4>
<p data-tool="mdnice编辑器">A: 说实话,刚开始使用 TS 的时候还是挺喜欢用 any 的,毕竟大家都是从 JS 过渡过来的,对这种影响效率的代码开发方式并不能完全接受,因此不管是出于偷懒还是找不到合适定义的情况,使用 any 的情况都比较多。</p>
<p data-tool="mdnice编辑器">随着使用时间的增加和对 TS 学习理解的加深,逐步离不开了 TS 带来的类型定义红利,不希望代码中出现 any,所有类型都必须要一个一个找到对应的定义,甚至已经丧失了裸写 JS 的勇气。</p>
<p data-tool="mdnice编辑器">这是一个目前没有正确答案的问题,总是要在效率和时间等等因素中找一个最适合自己的平衡。不过我还是推荐使用 TS,随着前端工程化演进和地位的提高,强类型语言一定是多人协作和代码健壮最可靠的保障之一,多用 TS,少用 any,也是前端界的一个普遍共识。</p>
<h4 data-tool="mdnice编辑器">Q: 类型定义文件(.d.ts)如何放置</h4>
<p data-tool="mdnice编辑器">A: 这个好像业界也没有特别统一的规范,我的想法如下:</p>
<ul class="list-paddingleft-2" data-tool="mdnice编辑器">
<li>临时的类型,直接在使用时定义</li>
</ul>
<p data-tool="mdnice编辑器">如自己写了一个组件内部的 Helper,函数的入参和出参只供内部使用也不存在复用的可能,可以直接在定义函数的时候就在后面定义。</p>
<pre data-tool="mdnice编辑器"><code>function&nbsp;format(input:&nbsp;{k:&nbsp;string}[]):&nbsp;number[]&nbsp;{&nbsp;/***/&nbsp;}<br></code></pre>
<ul class="list-paddingleft-2" data-tool="mdnice编辑器">
<li>组件个性化类型,直接定义在 ts(x)文件中</li>
</ul>
<p data-tool="mdnice编辑器">如 AntD 组件设计,每个单独组件的 Props、State 等专门定义了类型并 export 出去。</p>
<pre data-tool="mdnice编辑器"><code>//&nbsp;Table.tsx<br>export&nbsp;type&nbsp;TableProps&nbsp;=&nbsp;{&nbsp;/***/&nbsp;}<br>export&nbsp;type&nbsp;ColumnProps&nbsp;=&nbsp;{&nbsp;/***/&nbsp;}<br>export&nbsp;default&nbsp;function&nbsp;Table()&nbsp;{&nbsp;/***/&nbsp;}<br></code></pre>
<p data-tool="mdnice编辑器">这样使用者如果需要这些类型可以通过 import type 的方式引入来使用。</p>
<ul class="list-paddingleft-2" data-tool="mdnice编辑器">
<li>范围/全局数据,定义在.d.ts 文件中</li>
</ul>
<p data-tool="mdnice编辑器">全局类型数据,这个大家毫无异议,一般根目录下有个 typings 文件夹,里面会存放一些全局类型定义。</p>
<p data-tool="mdnice编辑器">假如我们使用了 css module,那么我们需要让 TS 识别.less 文件(或者.scss)引入后是一个对象,可以如此定义:</p>
<pre data-tool="mdnice编辑器"><code>declare&nbsp;module&nbsp;'*.less'&nbsp;{<br>&nbsp;&nbsp;const&nbsp;resource:&nbsp;{&nbsp;:&nbsp;string&nbsp;};<br>&nbsp;&nbsp;export&nbsp;=&nbsp;resource;<br>}<br></code></pre>
<p data-tool="mdnice编辑器">而对于一些全局的数据类型,如后端返回的通用的数据类型,我也习惯将其放在 typings 文件夹下,使用 Namespace 的方式来避免名字冲突,如此可以节省组件 import 类型定义的语句。</p>
<pre data-tool="mdnice编辑器"><code>declare&nbsp;namespace&nbsp;EdgeApi&nbsp;{<br>&nbsp;&nbsp;interface&nbsp;Department&nbsp;{<br>&nbsp;&nbsp;&nbsp;&nbsp;description:&nbsp;string;<br>&nbsp;&nbsp;&nbsp;&nbsp;gmt_create:&nbsp;string;<br>&nbsp;&nbsp;&nbsp;&nbsp;gmt_modify:&nbsp;string;<br>&nbsp;&nbsp;&nbsp;&nbsp;id:&nbsp;number;<br>&nbsp;&nbsp;&nbsp;&nbsp;name:&nbsp;string;<br>&nbsp;&nbsp;}<br>}<br></code></pre>
<p data-tool="mdnice编辑器">这样,每次使用的时候,只需要<code>const department: EdgeApi.Department</code>即可,节省了不少导入的精力。开发者只要能约定规范,避免命名冲突即可。</p>
<p data-tool="mdnice编辑器"><strong>关于 TS 用法的总结就结束到这里,感谢大家的观看~</strong></p>
<h3 data-tool="mdnice编辑器">参考资料</h3>
<p>&nbsp;TypeScript Handbook:&nbsp;</p>
<p>https://www.typescriptlang.org/docs/handbook/utility-types.html</p>
<p><br>&nbsp;utility-types:&nbsp;</p>
<p>https://github.com/piotrwitek/utility-types</p>
<p style="text-align: center">&nbsp;</p>
<p style="text-align: center">&nbsp;</p>
<p style="text-align: center"><img src="https://mmbiz.qpic.cn/mmbiz_png/ic5A4V8PX4PmYRMdf2zNTI5ibzzjibIRoBHXEFlX0W4k2AJCl5brCt69CEtoPJSErVxpKvNictXlq5ZOfiafEgCmScQ/640?wx_fmt=png" data-ratio="0.057692307692307696" data-type="png" data-w="208" data-width="100%">文章就分享到这,欢迎关注“前端大神之路”<img src="https://mmbiz.qpic.cn/mmbiz_png/ic5A4V8PX4PmYRMdf2zNTI5ibzzjibIRoBHXEFlX0W4k2AJCl5brCt69CEtoPJSErVxpKvNictXlq5ZOfiafEgCmScQ/640?wx_fmt=png" data-ratio="0.057692307692307696" data-type="png" data-w="208" data-width="100%"><em id="__mceDel">&nbsp;</em></p>
<p><img src="https://mmbiz.qpic.cn/mmbiz_jpg/ic5A4V8PX4PlFsF8IR1HCWm1d4NkicdsocH4vNZlgibKwBuf0HbtdpsC3a7YGvDIYGrgIM88ADywXHc7rPOGqibeqg/640?wx_fmt=jpeg" width="454" height="189" class="rich_pages" style="display: block; margin-left: auto; margin-right: auto" data-ratio="0.4166666666666667" data-s="300,640" data-type="jpeg" data-w="720"></p><br><br>
来源:https://www.cnblogs.com/cczlovexw/p/14389259.html
頁: [1]
查看完整版本: TypeScript 高级用法