唐新国 發表於 2020-1-22 22:04:00

编写TypeScript工具类型,你需要知道的知识

<h1 id="什么是工具类型">什么是工具类型</h1>
<p>用 JavaScript 编写中大型程序是离不开 <code>lodash</code> 工具的,而用 TypeScript 编程同样离不开工具类型的帮助,工具类型就是类型版的 <code>lodash</code> 。简单的来说,就是把已有的类型经过类型转换构造一个新的类型。工具类型本身也是类型,得益于泛型的帮助,使其能够对类型进行抽象的处理。工具类型主要目的是简化类型编程的过程,提高生产力。</p>
<h1 id="使用工具类型的好处">使用工具类型的好处</h1>
<p>先来看看一个场景,体会下工具类型带来什么好处。</p>
<pre><code class="language-typescript">// 一个用户接口
interface User {
name: string
avatar: string
country:string
friend:{
    name: string
    sex: string
}
}
</code></pre>
<p>现在业务要求 <code>User</code> 接口里的成员都变为可选,你会怎么做?再定义一个接口,为成员都加上可选修饰符吗?这种方法确实可行,但接口里有几十个成员呢?此时,工具类型就可以派上用场。</p>
<pre><code class="language-typescript">type Partial&lt;T&gt; = {?: T}
type PartialUser = Partial&lt;User&gt;

// 此时PartialUser等同于
type PartialUser = {
name?: string | undefined;
avatar?: string | undefined;
country?: string | undefined;
friend?: {
    name: string;
    sex: string;
} | undefined;
}
</code></pre>
<p>通过工具类型的处理,我们得到一个新的类型。即使成员有成千上百个,我们也只需要一行代码。由于 <code>friend</code> 成员是对象,上面的 <code>Partial</code> 处理只对第一层添加可选修饰符,假如需要将对象成员内的成员也添加可选修饰符,可以使用 <code>Partial</code> 递归来解决。</p>
<pre><code class="language-typescript">type partial&lt;T&gt; = {
?: T extends object ? partial&lt;T&gt; : T
}
</code></pre>
<p>如果你是第一次看到以上的写法,可能会很懵逼,不知道发生了什么操作。不慌,且往下看,或许当你看完这篇文章再回过头来看时,会发现原来是这么一回事。</p>
<h1 id="关键字">关键字</h1>
<p>TypeScript 中的一些关键字对于编写工具类型必不可缺</p>
<h2 id="keyof">keyof</h2>
<p>语法: <strong>keyof T</strong> 。返回联合类型,为 <code>T</code> 的所有 <code>key</code></p>
<pre><code class="language-typescript">interface User{
name: string
age: number
}

type Man = {
name:string,
height: 180
}

type ManKeys = keyof Man // "name" | "height"
type UserKeys = keyof User // "name" | "age"
</code></pre>
<h2 id="typeof">typeof</h2>
<p>语法: <strong>typeof T</strong> 。返回 <code>T</code> 的成员的类型</p>
<pre><code class="language-typescript">let arr = ['apple', 'banana', 100]
let man = {
name: 'Jeo',
age: 20,
height: 180
}

type Arr = typeof arr // (string | number)[]
type Man = typeof man // {name: string; age: number; height: number;}
</code></pre>
<h2 id="infer">infer</h2>
<p>相比上面两个关键字, <code>infer</code> 的使用可能会有点难理解。在有条件类型的 <code>extends</code> 子语句中,允许出现 <code>infer</code> 声明,它会引入一个待推断的类型变量。这个推断的类型变量可以在有条件类型的 <code>true</code> 分支中被引用。</p>
<p>简单来说,它可以把类型处理过程的某个部分抽离出来当做类型变量。以下例子需要结合高级类型,如果不能理解,可以选择跳转这部分,把高级类型看完后再回来。</p>
<p>下面代码会提取函数类型的返回值类型:</p>
<pre><code class="language-typescript">type ReturnType&lt;T&gt; = T extends (...args: any[]) =&gt; infer R ? R : any;
</code></pre>
<p><code>(...args: any[]) =&gt; infer R</code> 和 <code>Function</code> 类型的作用是差不多的,这样写只是为了能够在过程中拿到函数的返回值类型。 <code>infer</code> 在这里相当于把返回值类型声明成一个类型变量,提供给后面的过程使用。</p>
<p>有条件类型可以嵌套来构成一系列的匹配模式,按顺序进行求值:</p>
<pre><code class="language-typescript">type Unpacked&lt;T&gt; =
T extends (infer U)[] ? U :
T extends (...args: any[]) =&gt; infer U ? U :
T extends Promise&lt;infer U&gt; ? U :
T;

type T0 = Unpacked&lt;string&gt;;// string
type T1 = Unpacked&lt;string[]&gt;;// string
type T2 = Unpacked&lt;() =&gt; string&gt;;// string
type T3 = Unpacked&lt;Promise&lt;string&gt;&gt;;// string
type T4 = Unpacked&lt;Promise&lt;string&gt;[]&gt;;// Promise&lt;string&gt;
type T5 = Unpacked&lt;Unpacked&lt;Promise&lt;string&gt;[]&gt;&gt;;// string
</code></pre>
<h1 id="高级类型">高级类型</h1>
<h2 id="交叉类型">交叉类型</h2>
<p>语法: <strong>A &amp; B</strong> ,交叉类型可以把多个类型合并成一个新类型,新类型将拥有所有类型的成员。</p>
<pre><code class="language-typescript">interface Shape {
size: string
color: string
}
interface Brand {
name: string
price: number
}

let clothes: Shape&amp;Brand = {
name: 'Uniqlo',
color: 'blue',
size: 'XL',
price: 200
}
</code></pre>
<h2 id="联合类型">联合类型</h2>
<p>语法: <strong>typeA | typeB</strong> ,联合类型是包含多种类型的类型,被绑定联合类型的成员只需满足其中一种类型。</p>
<pre><code class="language-typescript">function pushItem(item:string|number){
let array:Array&lt;string|number&gt; = ['apple','banana','cherry']
array.push(item)
}
pushItem(10) // ok
pushItem('durian') // ok
</code></pre>
<p>通常,删除用户信息需要提供 <code>id</code> ,创建用户则不需要 <code>id</code> 。这种类型应该如何定义?如果选择为 <code>id</code> 字段提供添加可选修饰符的话,那就太不明智了。因为在删除用户时,即使不填写 <code>id</code> 属性也不会报错,这不是我们想要的结果。</p>
<p>可辨识联合类型能帮助我们解决这个问题:</p>
<pre><code class="language-typescript">type UserAction = {
action: 'create'
}|{
id:number
action: 'delete'
}
let userAction:UserAction = {
id: 1,
action: 'delete'
}
</code></pre>
<h2 id="字面量类型">字面量类型</h2>
<p>字⾯量类型主要分为 真值字⾯量类型,数字字⾯量类型,枚举字⾯量类型,⼤整数字⾯量类型、字符串字⾯量类型。</p>
<pre><code class="language-typescript">const a: 2333 = 2333 // ok
const b: 0b10 = 2 // ok
const c: 0x514 = 0x514 // ok
const d: 'apple' = 'apple' // ok
const e: true = true // ok
const f: 'apple' = 'banana' // 不能将类型“"banana"”分配给类型“"apple"”
</code></pre>
<p>下面以字符串字面量类型作为例子:</p>
<p>字符串字面量类型允许指定的字符串作为类型。如果使用 JavaScript 的模式中看下面的例子,会把 <code>level</code> 当成一个值。但在 TypeScript 中,千万不要用这种思维去看待, <code>level</code> 表示的就是一个字符串 <code>coder</code> 的类型,被绑定这个类型的变量,它的值只能是 <code>coder</code> 。</p>
<pre><code class="language-typescript">type Level = 'coder'
let level:Level = 'coder' // ok
let level2:Level = 'programmer' // 不能将类型“"programmer"”分配给类型“"coder"”
</code></pre>
<p>字符串和联合类型搭配,可以实现类似枚举类型的字符串</p>
<pre><code class="language-typescript">type Level = 'coder' | 'leader' | 'boss'
function getWork(level: Level){
if(level === 'coder'){
    console.log('打代码、摸鱼')
}else if(level === 'leader'){
    console.log('造轮子、架构')
}else if(level === 'boss'){
    console.log('喝茶、谈生意')
}
}
getWork('coder')
getWork('user') // 类型“"user"”的参数不能赋给类型“Level”的参数
</code></pre>
<h2 id="索引类型">索引类型</h2>
<p>语法: <strong>T</strong> ,使用索引类型,编译器就能够检查使用动态属性名的代码。在 JavaScript 中,对象可以用属性名获取值,而在 TypeScript 中,这一切被抽象化,变成通过索引获取类型。就像 <code>person</code> 被抽象成类型 <code>Person</code> ,在以下例子中代表的就是 <code>string</code> 类型。</p>
<pre><code class="language-typescript">interface Person {
name: string;
age: number;
}
let person: Person = {
name: 'Jeo',
age: 20
}
let name = person['name'] // 'Jeo'
type str = Person['name'] // string
</code></pre>
<p>我们可以在普通的上下文里使用 <code>T</code> ,只要确保类型变量 <code>K</code> 为 <code>T</code> 的索引即可</p>
<pre><code class="language-typescript">function getProperty&lt;T, K extends keyof T&gt;(o: T, name: K): T {
return o; // o is of type T
}
</code></pre>
<p><code>getProperty</code> 里的 <code>o: T</code> 和 <code>name: K</code> ,意味着 <code>o: T</code></p>
<pre><code class="language-typescript">let name: string = getProperty(person, 'name');
let age: number = getProperty(person, 'age');
let unknown = getProperty(person, 'unknown'); // 类型“"unknown"”的参数不能赋给类型“"name" | "age"”的参数
</code></pre>
<p><code>K</code> 不仅可以传成员,成员的字符串联合类型也是有效的</p>
<pre><code class="language-typescript">type Union = Person // "string" | "number"
</code></pre>
<h2 id="映射类型">映射类型</h2>
<p>语法: <strong></strong> 。TypeScript 提供了从旧类型中创建新类型的一种方式 。在映射类型里,新类型以相同的形式去转换旧类型里每个属性。根据 <code>Keys</code> 来创建类型, <code>Keys</code> 有效值为 string | number | symbol 或 联合类型。</p>
<pre><code class="language-typescript">type Keys = 'name'|10
type User = {
: string
}
</code></pre>
<p>该语法可以理解为内部使用了循环</p>
<ul>
<li>K: 依次绑定到每个属性,相当于 Keys 的项</li>
<li>Keys: 包含要迭代的属性名的集合</li>
</ul>
<p>因此以上的例子等同于:</p>
<pre><code class="language-typescript">type User = {
name: string;
10: string;
}
</code></pre>
<p>需要注意的是这个语法描述的是类型而非成员。若想添加额外的成员,需使用交叉类型:</p>
<pre><code class="language-typescript">// 这样使用
type ReadonlyWithNewMember&lt;T&gt; = {
readonly : T;
} &amp; { newMember: boolean }
// 不要这样使用
// 这会报错!
type ReadonlyWithNewMember&lt;T&gt; = {
readonly : T;
newMember: boolean;
}
</code></pre>
<p>在真正应用中,映射类型结合索引访问类型是一个很好的搭配。因为转换过程会基于一些已存在的类型,且按照一定的方式转换字段。你可以把这过程理解为 JavaScript 中数组的 <code>map</code> 方法,在原本的基础上扩展元素( TypeScript 中指类型),当然这种理解过程可能有点粗糙。</p>
<p>文章开头的 <code>Partial</code> 工具类型正是使用这种搭配,为原有的类型添加可选修饰符。</p>
<h2 id="条件类型">条件类型</h2>
<p>语法: <strong>T extends U ? X : Y</strong> ,若 <code>T</code> 能够赋值给 <code>U</code> ,那么类型是 <code>X</code> ,否则为 <code>Y</code> 。条件类型以条件表达式推断类型关系,选择其中一个分支。相对上面的类型,条件类型很好理解,类似 JavaScript 中的三目运算符。</p>
<p>再来看看文章开头递归的操作,你就会发现能看懂这段处理过程。过程:使用映射类型遍历,判断 <code>T</code> 属于 <code>object</code> 类型,则把 <code>T</code> 传入 <code>partial</code> 递归,否则返回类型 <code>T</code> 。</p>
<pre><code class="language-typescript">type partial&lt;T&gt; = {
?: T extends object ? partial&lt;T&gt; : T
}
</code></pre>
<h2 id="小结">小结</h2>
<p>关于一些常用的高级类型相信大家都了解得差不多,下面将应用这些类型来编写一个工具类型。</p>
<p>该工具类型实现的功能为筛选出两个 <code>interface</code> 的公共成员:</p>
<pre><code class="language-typescript">interface PersonA{
name: string
age: number
boyfriend: string
car: {
    type: 'Benz'
}
}

interface PersonB{
name: string
age: string
girlfriend: string
car: {
    type: 'bicycle'
}
}

type Filter&lt;T,U&gt; = T extends U ? T : never

type Common&lt;A, B&gt; = {
: A extends B ? A : A|B
}
</code></pre>
<p>通过 <code>Filter</code> 筛选出公共的成员联合类型 <code>"name"|"age"</code> 作为映射类型的集合,公共部分可能会存在类型不同的情况,因此要为成员保留两者的类型。</p>
<pre><code class="language-typescript">type CommonMember = Common&lt;PersonA, PersonB&gt;

// 等同于
type CommonMember = {
name: string;
age: string | number;
car: {
    type: "Benz";
} | {
    type: "bicycle";
};
}
</code></pre>
<h1 id="内置工具类型">内置工具类型</h1>
<p>为了满足常见的类型转换需求, TypeScript 也提供一些内置工具类型,这些类型是全局可见的。</p>
<h2 id="partial">Partial<t></t></h2>
<p>构造类型 <code>T</code> ,并将它所有的属性设置为可选的。它的返回类型表示输入类型的所有子类型。</p>
<pre><code class="language-typescript">interface Todo {
title: string;
description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial&lt;Todo&gt;) {
return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
title: 'organize desk',
description: 'clear clutter',
};

const todo2 = updateTodo(todo1, {
description: 'throw out trash',
});
</code></pre>
<h2 id="readonly">Readonly<t></t></h2>
<p>构造类型T,并将它所有的属性设置为readonly,也就是说构造出的类型的属性不能被再次赋值。</p>
<pre><code class="language-typescript">interface Todo {
title: string;
}

const todo: Readonly&lt;Todo&gt; = {
title: 'Delete inactive users',
};

todo.title = 'Hello'; // Error: cannot reassign a readonly property
</code></pre>
<h2 id="recordk-t">Record&lt;K, T&gt;</h2>
<p>构造一个类型,其属性名的类型为K,属性值的类型为T。这个工具可用来将某个类型的属性映射到另一个类型上。</p>
<pre><code class="language-typescript">interface PageInfo {
title: string;
}

type Page = 'home' | 'about' | 'contact';

const x: Record&lt;Page, PageInfo&gt; = {
about: { title: 'about' },
contact: { title: 'contact' },
home: { title: 'home' },
};
</code></pre>
<h2 id="pickt-k">Pick&lt;T, K&gt;</h2>
<p>从类型T中挑选部分属性K来构造类型。</p>
<pre><code class="language-typescript">interface Todo {
title: string;
description: string;
completed: boolean;
}

type TodoPreview = Pick&lt;Todo, 'title' | 'completed'&gt;;

const todo: TodoPreview = {
title: 'Clean room',
completed: false,
};
</code></pre>
<h2 id="omitt-k">Omit&lt;T, K&gt;</h2>
<p>从类型T中剔除部分属性K来构造类型,与Pick相反。</p>
<pre><code class="language-typescript">interface Todo {
title: string;
description: string;
completed: boolean;
}

type TodoPreview = Omit&lt;Todo, 'title' | 'completed'&gt;;

const todo: TodoPreview = {
description: 'I am description'
};
</code></pre>
<h2 id="excludet-u">Exclude&lt;T, U&gt;</h2>
<p>从类型T中剔除所有可以赋值给U的属性,然后构造一个类型。</p>
<pre><code class="language-typescript">type T0 = Exclude&lt;"a" | "b" | "c", "a"&gt;;// "b" | "c"
type T1 = Exclude&lt;"a" | "b" | "c", "a" | "b"&gt;;// "c"
type T2 = Exclude&lt;string | number | (() =&gt; void), Function&gt;;// string | number
</code></pre>
<h2 id="extractt-u">Extract&lt;T, U&gt;</h2>
<p>从类型T中提取所有可以赋值给U的类型,然后构造一个类型。</p>
<pre><code class="language-typescript">type T0 = Extract&lt;"a" | "b" | "c", "a" | "f"&gt;;// "a"
type T1 = Extract&lt;string | number | (() =&gt; void), Function&gt;;// () =&gt; void
</code></pre>
<h2 id="nonnullable">NonNullable<t></t></h2>
<p>从类型T中剔除null和undefined,然后构造一个类型。</p>
<pre><code class="language-typescript">type T0 = NonNullable&lt;string | number | undefined&gt;;// string | number
type T1 = NonNullable&lt;string[] | null | undefined&gt;;// string[]
</code></pre>
<h2 id="returntype">ReturnType<t></t></h2>
<p>由函数类型T的返回值类型构造一个类型。</p>
<pre><code class="language-typescript">type T0 = ReturnType&lt;() =&gt; string&gt;;// string
type T1 = ReturnType&lt;(s: string) =&gt; void&gt;;// void
type T2 = ReturnType&lt;(&lt;T&gt;() =&gt; T)&gt;;// {}
type T3 = ReturnType&lt;(&lt;T extends U, U extends number[]&gt;() =&gt; T)&gt;;// number[]
type T5 = ReturnType&lt;any&gt;;// any
type T6 = ReturnType&lt;never&gt;;// any
type T7 = ReturnType&lt;string&gt;;// Error
type T8 = ReturnType&lt;Function&gt;;// Error
</code></pre>
<h2 id="instancetype">InstanceType<t></t></h2>
<p>由构造函数类型T的实例类型构造一个类型。</p>
<pre><code class="language-typescript">class C {
x = 0;
y = 0;
}

type T0 = InstanceType&lt;typeof C&gt;;// C
type T1 = InstanceType&lt;any&gt;;// any
type T2 = InstanceType&lt;never&gt;;// any
type T3 = InstanceType&lt;string&gt;;// Error
type T4 = InstanceType&lt;Function&gt;;// Error

let t0:T0 = {
x: 10,
y: 2
}
</code></pre>
<h2 id="required">Required<t></t></h2>
<p>构造一个类型,使类型T的所有属性为required。</p>
<pre><code class="language-typescript">interface Props {
a?: number;
b?: string;
};

const obj: Props = { a: 5 }; // OK

const obj2: Required&lt;Props&gt; = { a: 5 }; // Error: property 'b' missing
</code></pre>
<h1 id="写在最后">写在最后</h1>
<p>除了介绍编写工具类型所需要具备的一些知识点,以及 TypeScript 内置的工具类型。更重要的是抽象思维能力,不难发现上面的例子大部分没有具体的值运算,都是使用类型在编程。想要理解这些知识,必须要进入到抽象逻辑里思考。还有高级类型的搭配和类型转换的处理,也要通过大量的实践才能玩好。说实话,自己学习这些知识时,真正感受到 TypeScript 的深不可测,也了解到自身的不足之处。突然想起在某篇文章的一句话:技术是无止尽的,接触的越多,越能感到自己的渺小。</p>
<h1 id="参考资料">参考资料</h1>
<p>Typescript Hankbook(中文版)</p>


</div>
<div id="MySignature" role="contentinfo">
    <div>作者:WahFung</div>
<div>出处:http://www.cnblogs.com/chanwahfung/</div>
<div>本文版权归作者和博客园共有,转载请贴出原文链接,并保留此段声明,否则保留追究法律责任的权利。 </div><br><br>
来源:https://www.cnblogs.com/chanwahfung/p/12229788.html
頁: [1]
查看完整版本: 编写TypeScript工具类型,你需要知道的知识