用 JavaScript 编写中大型程序是离不开 lodash
工具的,而用 TypeScript 编程一样离不开工具类型的帮助,工具类型就是类型版的 lodash
。简单的来讲,就是把已有的类型通过类型转换构造一个新的类型。工具类型自己也是类型,得益于泛型的帮助,使其可以对类型进行抽象的处理。工具类型主要目的是简化类型编程的过程,提升生产力。html
先来看看一个场景,体会下工具类型带来什么好处。git
// 一个用户接口 interface User { name: string avatar: string country:string friend:{ name: string sex: string } }
如今业务要求 User
接口里的成员都变为可选,你会怎么作?再定义一个接口,为成员都加上可选修饰符吗?这种方法确实可行,但接口里有几十个成员呢?此时,工具类型就能够派上用场。typescript
type Partial<T> = {[K in keyof T]?: T[K]} type PartialUser = Partial<User> // 此时PartialUser等同于 type PartialUser = { name?: string | undefined; avatar?: string | undefined; country?: string | undefined; friend?: { name: string; sex: string; } | undefined; }
经过工具类型的处理,咱们获得一个新的类型。即便成员有成千上百个,咱们也只须要一行代码。因为 friend
成员是对象,上面的 Partial
处理只对第一层添加可选修饰符,假如须要将对象成员内的成员也添加可选修饰符,可使用 Partial
递归来解决。编程
type partial<T> = { [K in keyof T]?: T[K] extends object ? partial<T[K]> : T[K] }
若是你是第一次看到以上的写法,可能会很懵逼,不知道发生了什么操做。不慌,且往下看,或许当你看完这篇文章再回过头来看时,会发现原来是这么一回事。数组
TypeScript 中的一些关键字对于编写工具类型必不可缺架构
语法: keyof T 。返回联合类型,为 T
的全部 key
app
interface User{ name: string age: number } type Man = { name:string, height: 180 } type ManKeys = keyof Man // "name" | "height" type UserKeys = keyof User // "name" | "age"
语法: typeof T 。返回 T
的成员的类型函数
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;}
相比上面两个关键字, infer
的使用可能会有点难理解。在有条件类型的 extends
子语句中,容许出现 infer
声明,它会引入一个待推断的类型变量。这个推断的类型变量能够在有条件类型的 true
分支中被引用。工具
简单来讲,它能够把类型处理过程的某个部分抽离出来当作类型变量。如下例子须要结合高级类型,若是不能理解,能够选择跳转这部分,把高级类型看完后再回来。学习
下面代码会提取函数类型的返回值类型:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
(...args: any[]) => infer R
和 Function
类型的做用是差很少的,这样写只是为了可以在过程当中拿到函数的返回值类型。 infer
在这里至关于把返回值类型声明成一个类型变量,提供给后面的过程使用。
有条件类型能够嵌套来构成一系列的匹配模式,按顺序进行求值:
type Unpacked<T> = T extends (infer U)[] ? U : T extends (...args: any[]) => infer U ? U : T extends Promise<infer U> ? U : T; type T0 = Unpacked<string>; // string type T1 = Unpacked<string[]>; // string type T2 = Unpacked<() => string>; // string type T3 = Unpacked<Promise<string>>; // string type T4 = Unpacked<Promise<string>[]>; // Promise<string> type T5 = Unpacked<Unpacked<Promise<string>[]>>; // string
语法: A & B ,交叉类型能够把多个类型合并成一个新类型,新类型将拥有全部类型的成员。
interface Shape { size: string color: string } interface Brand { name: string price: number } let clothes: Shape&Brand = { name: 'Uniqlo', color: 'blue', size: 'XL', price: 200 }
语法: typeA | typeB ,联合类型是包含多种类型的类型,被绑定联合类型的成员只需知足其中一种类型。
function pushItem(item:string|number){ let array:Array<string|number> = ['apple','banana','cherry'] array.push(item) } pushItem(10) // ok pushItem('durian') // ok
一般,删除用户信息须要提供 id
,建立用户则不须要 id
。这种类型应该如何定义?若是选择为 id
字段提供添加可选修饰符的话,那就太不明智了。由于在删除用户时,即便不填写 id
属性也不会报错,这不是咱们想要的结果。
可辨识联合类型能帮助咱们解决这个问题:
type UserAction = { action: 'create' }|{ id:number action: 'delete' } let userAction:UserAction = { id: 1, action: 'delete' }
字⾯量类型主要分为 真值字⾯量类型,数字字⾯量类型,枚举字⾯量类型,⼤整数字⾯量类型、字符串字⾯量类型。
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"”
下面以字符串字面量类型做为例子:
字符串字面量类型容许指定的字符串做为类型。若是使用 JavaScript 的模式中看下面的例子,会把 level
当成一个值。但在 TypeScript 中,千万不要用这种思惟去看待, level
表示的就是一个字符串 coder
的类型,被绑定这个类型的变量,它的值只能是 coder
。
type Level = 'coder' let level:Level = 'coder' // ok let level2:Level = 'programmer' // 不能将类型“"programmer"”分配给类型“"coder"”
字符串和联合类型搭配,能够实现相似枚举类型的字符串
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”的参数
语法: T[K] ,使用索引类型,编译器就可以检查使用动态属性名的代码。在 JavaScript 中,对象能够用属性名获取值,而在 TypeScript 中,这一切被抽象化,变成经过索引获取类型。就像 person[name]
被抽象成类型 Person[name]
,在如下例子中表明的就是 string
类型。
interface Person { name: string; age: number; } let person: Person = { name: 'Jeo', age: 20 } let name = person['name'] // 'Jeo' type str = Person['name'] // string
咱们能够在普通的上下文里使用 T[K]
,只要确保类型变量 K
为 T
的索引便可
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] { return o[name]; // o[name] is of type T[K] }
getProperty
里的 o: T
和 name: K
,意味着 o[name]: T[K]
let name: string = getProperty(person, 'name'); let age: number = getProperty(person, 'age'); let unknown = getProperty(person, 'unknown'); // 类型“"unknown"”的参数不能赋给类型“"name" | "age"”的参数
K
不只能够传成员,成员的字符串联合类型也是有效的
type Union = Person[keyof Person] // "string" | "number"
语法: [K in Keys] 。TypeScript 提供了从旧类型中建立新类型的一种方式 。在映射类型里,新类型以相同的形式去转换旧类型里每一个属性。根据 Keys
来建立类型, Keys
有效值为 string | number | symbol 或 联合类型。
type Keys = 'name'|10 type User = { [K in Keys]: string }
该语法能够理解为内部使用了循环
所以以上的例子等同于:
type User = { name: string; 10: string; }
须要注意的是这个语法描述的是类型而非成员。若想添加额外的成员,需使用交叉类型:
// 这样使用 type ReadonlyWithNewMember<T> = { readonly [P in keyof T]: T[P]; } & { newMember: boolean } // 不要这样使用 // 这会报错! type ReadonlyWithNewMember<T> = { readonly [P in keyof T]: T[P]; newMember: boolean; }
在真正应用中,映射类型结合索引访问类型是一个很好的搭配。由于转换过程会基于一些已存在的类型,且按照必定的方式转换字段。你能够把这过程理解为 JavaScript 中数组的 map
方法,在本来的基础上扩展元素( TypeScript 中指类型),固然这种理解过程可能有点粗糙。
文章开头的 Partial
工具类型正是使用这种搭配,为原有的类型添加可选修饰符。
语法: T extends U ? X : Y ,若 T
可以赋值给 U
,那么类型是 X
,不然为 Y
。条件类型以条件表达式推断类型关系,选择其中一个分支。相对上面的类型,条件类型很好理解,相似 JavaScript 中的三目运算符。
再来看看文章开头递归的操做,你就会发现能看懂这段处理过程。过程:使用映射类型遍历,判断 T[K]
属于 object
类型,则把 T[K]
传入 partial
递归,不然返回类型 T[K]
。
type partial<T> = { [K in keyof T]?: T[K] extends object ? partial<T[K]> : T[K] }
关于一些经常使用的高级类型相信你们都了解得差很少,下面将应用这些类型来编写一个工具类型。
该工具类型实现的功能为筛选出两个 interface
的公共成员:
interface PersonA{ name: string age: number boyfriend: string car: { type: 'Benz' } } interface PersonB{ name: string age: string girlfriend: string car: { type: 'bicycle' } } type Filter<T,U> = T extends U ? T : never type Common<A, B> = { [K in Filter<keyof A, keyof B>]: A[K] extends B[K] ? A[K] : A[K]|B[K] }
经过 Filter
筛选出公共的成员联合类型 "name"|"age"
做为映射类型的集合,公共部分可能会存在类型不一样的状况,所以要为成员保留二者的类型。
type CommonMember = Common<PersonA, PersonB> // 等同于 type CommonMember = { name: string; age: string | number; car: { type: "Benz"; } | { type: "bicycle"; }; }
为了知足常见的类型转换需求, TypeScript 也提供一些内置工具类型,这些类型是全局可见的。
构造类型 T
,并将它全部的属性设置为可选的。它的返回类型表示输入类型的全部子类型。
interface Todo { title: string; description: string; } function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) { return { ...todo, ...fieldsToUpdate }; } const todo1 = { title: 'organize desk', description: 'clear clutter', }; const todo2 = updateTodo(todo1, { description: 'throw out trash', });
构造类型T,并将它全部的属性设置为readonly,也就是说构造出的类型的属性不能被再次赋值。
interface Todo { title: string; } const todo: Readonly<Todo> = { title: 'Delete inactive users', }; todo.title = 'Hello'; // Error: cannot reassign a readonly property
构造一个类型,其属性名的类型为K,属性值的类型为T。这个工具可用来将某个类型的属性映射到另外一个类型上。
interface PageInfo { title: string; } type Page = 'home' | 'about' | 'contact'; const x: Record<Page, PageInfo> = { about: { title: 'about' }, contact: { title: 'contact' }, home: { title: 'home' }, };
从类型T中挑选部分属性K来构造类型。
interface Todo { title: string; description: string; completed: boolean; } type TodoPreview = Pick<Todo, 'title' | 'completed'>; const todo: TodoPreview = { title: 'Clean room', completed: false, };
从类型T中剔除部分属性K来构造类型,与Pick相反。
interface Todo { title: string; description: string; completed: boolean; } type TodoPreview = Omit<Todo, 'title' | 'completed'>; const todo: TodoPreview = { description: 'I am description' };
从类型T中剔除全部能够赋值给U的属性,而后构造一个类型。
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c" type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c" type T2 = Exclude<string | number | (() => void), Function>; // string | number
从类型T中提取全部能够赋值给U的类型,而后构造一个类型。
type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a" type T1 = Extract<string | number | (() => void), Function>; // () => void
从类型T中剔除null和undefined,而后构造一个类型。
type T0 = NonNullable<string | number | undefined>; // string | number type T1 = NonNullable<string[] | null | undefined>; // string[]
由函数类型T的返回值类型构造一个类型。
type T0 = ReturnType<() => string>; // string type T1 = ReturnType<(s: string) => void>; // void type T2 = ReturnType<(<T>() => T)>; // {} type T3 = ReturnType<(<T extends U, U extends number[]>() => T)>; // number[] type T5 = ReturnType<any>; // any type T6 = ReturnType<never>; // any type T7 = ReturnType<string>; // Error type T8 = ReturnType<Function>; // Error
由构造函数类型T的实例类型构造一个类型。
class C { x = 0; y = 0; } type T0 = InstanceType<typeof C>; // C type T1 = InstanceType<any>; // any type T2 = InstanceType<never>; // any type T3 = InstanceType<string>; // Error type T4 = InstanceType<Function>; // Error let t0:T0 = { x: 10, y: 2 }
构造一个类型,使类型T的全部属性为required。
interface Props { a?: number; b?: string; }; const obj: Props = { a: 5 }; // OK const obj2: Required<Props> = { a: 5 }; // Error: property 'b' missing
除了介绍编写工具类型所须要具有的一些知识点,以及 TypeScript 内置的工具类型。更重要的是抽象思惟能力,不难发现上面的例子大部分没有具体的值运算,都是使用类型在编程。想要理解这些知识,必需要进入到抽象逻辑里思考。还有高级类型的搭配和类型转换的处理,也要经过大量的实践才能玩好。说实话,本身学习这些知识时,真正感觉到 TypeScript 的深不可测,也了解到自身的不足之处。忽然想起在某篇文章的一句话:技术是无止尽的,接触的越多,越能感到本身的眇小。