Typescript 高阶类型

大部分示例和教程来自优秀的 A. Sharif 的系列文章html

泛型 和 extends

function prop(obj, key) {
  return obj[key]
}
复制代码

这样一个函数如何让其类型安全呢?typescript

function prop<T, Key extends keyof T>(obj: T, key: Key) {
  return obj[key];
}

const user: User = {
  id: 1,
  name: "Test User",
  points: 0
};

const userName = prop(user, "name"); // const userName : string;
复制代码

须要像这样使用到泛型,由于咱们不知道这个 obj 的类型定义是怎样的,因此咱们须要使用 T 来做为 obj 的定义的占位,T 是第一个泛型参数。Key 是第二个泛型参数,它 extends 自 T 的全部 key 值,Key 在这里就是 "id" | "name" | "points" 的全部子集。express

在使用的时候咱们能够不用显式的传入泛型参数,由于咱们给函数传参的时候,TS 编译器就已经帮咱们推导出来正确的泛型参数了,而且验证了第二个参数的类型必须是 extends 自 T 的全部 key 值。这就为咱们提供了鲁棒性很是好的,类型安全的函数了,结合 vscode 还有自动补全的功能哦。json

Conditional Types

条件类型自 Typescript 2.8 引入。 它的定义是安全

"A conditional type selects one of two possible types based on a condition expressed as a type relationship test"app

type S = string extends any ? "string" : never;

/* type S = "string" */
复制代码

这个三元操做符的解释是若是 string 类型 extends 自 any 类型,(答案是明显的YES)。那么类型 S 的定义就是 string,不然就是 never函数

常见的应用就是标准库中 Exclude 的定义。学习

/** * Exclude from T those types that are assignable to U */
type Exclude<T, U> = T extends U ? never : T;
复制代码

Exclude可用于两个联合类型之间取差集。ui

Pick 和 Omit

lodash 库中有两个实用的方法,pickomit.es5

var object = { 'a': 1, 'b': '2', 'c': 3 };
 
_.pick(object, ['a', 'c']);
// => { 'a': 1, 'c': 3 }
复制代码

pick 用于由选取的对象属性组成新的对象。

var object = { 'a': 1, 'b': '2', 'c': 3 };
 
_.omit(object, ['a', 'c']);
// => { 'b': '2' }
复制代码

omit 的释义是消除。与 pick 相反; 此方法建立一个对象,消除指定的属性,剩下的对象属性组成一个新的对象。

Pick

在 Typescript 中也有相似的概念。只不过被操做的对象不是对象字面量,而是类型.

Pick 在 Typescript 的标准库中的定义是

type Pick<T, K extends keyof T> = { 
    [P in K]: T[P];
}
复制代码

能够看出 Pick 也是一种类型,可用于创造新的类型。这个新的类型的 key 值为泛型 T 的全部 key 值的联合类型的子集。

interface Person {
    name: string;
    age: number;
    location: string;
}
type PersonWithouLocation = Pick<Person, 'name' | 'age'>
// => type PersonWithouLocation = {
// name: string;
// age: number;
// }
复制代码

那如何为 _.pick 加上类型呢?

declare function pick<T extends object, K extends keyof T>(object: T, paths?: K[]): Pick<T, K> var object = { 'a': 1, 'b': '2', 'c': 3 };
const result = pick(object, ['a', 'c']); 
// const result: Pick<{
// 'a': number;
// 'b': string;
// 'c': number;
// }, "a" | "b">
复制代码

使用泛型和 Pick ,咱们就获得了一个类型安全的 pick 方法

Omit

在 Typescript 3.5 版本才引入 Omit 类型。

Omit 做为 Pick 的反操做,咱们想获得和上面 Pick 相同的操做只须要对类型的键值取反便可。

interface Person {
    name: string;
    age: number;
    location: string;
}
type PersonWithouLocation = Omit<Person, 'location'>
// 结果是
// type PersonWithouLocation = {
// name: string;
// age: number;
// }
复制代码

Omit 在标准库中的定义是

/** * Construct a type with the properties of T except for those in type K. */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
复制代码

让咱们看看 Omit 的实现,就是 Pick 出 T 中 keyof T 和 K 的差集组成新的类型。也就是说消除了第二个参数中为键值的类型组成新的类型。

Omit 实现中的第二个泛型参数为何不是 K extends keyof T,而是使用的 K extends keyof any呢?这样 IDE 就不能在第二个参数帮助补全了,这是个人一个疑问。在 Stack Overflow 上有找到 key 值多是 string | number | symbol 三种类型。

那如何为 _.omit 加上类型呢?

declare function omit<T extends object, K extends keyof T>(object: T, paths: K[]): Omit<T, K>

const result = omit(object, ['a', 'b']); 
// const result: Pick<{
//     'a': number;
//     'b': string;
//     'c': number;
// }, "c">
复制代码

扩展一下,也许某些时候 omit 方法的第二个参数多是扩展运算符。例如

function omit(object, ...rest) {
    // do omit
}
omit(object, 'a', 'b')
复制代码

那这个时候的类型定义应该是这样

declare function omit<T extends object, K extends keyof T>(object: T, ...paths: K[]): Omit<T, K> 复制代码

infer

infer 是 TS 的一个关键字,用于显式类型推断。与之相关的常见的两个类型就是 ReturnTypeParameters

function getInt(a: string) {
  return parseInt(a);
}

type A = ReturnType<typeof getInt>; // => number
复制代码

再这个例子中,咱们先须要使用 typeof 关键字获取函数的类型定义,也就是 (a: string) => number, 而后再将其做为泛型参数传入 ReturnType。这样就能从一个函数声明获得它的返回类型,这能够在写代码的时候减小咱们一些心智负担,可以很是灵活的获取类型。

ReturnType 在标准库中的实现是

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
复制代码

对这个实现的解释是,若是类型 T 是扩展自函数类型,那么返回值就是 infer 关键字推断出的 R 类型,若是不是,就返回 any 类型。

根据这个实现咱们还能实现一个高阶类型用于推断函数参数。

type ParametersType<T> = T extends (...args: infer K) => any ? K : any;
复制代码

使用这个类型推断出的类型是参数元组类型。

其实这个类型在标准库中的的实现是这样的,只不过这个标准库实现了限定了泛型入参的类型必须为函数类型。

/** * Obtain the parameters of a function type in a tuple */
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
复制代码

lib.es5.d.ts 中还实现了不少相似的高阶类型。可参考 官网

Mapped types 映射类型

常见的 Mapped types 都很简单, 能够简单过一下

Readonly

假设咱们有以下类型。

type User = {
  readonly id: number;
  name: string;
}
复制代码
type Readonly<Type> = {readonly [key in keyof Type ]: Type[key]};
type ReadonlyUser =  Readonly<User>;
复制代码

Readonly 的实现就是让泛型的每个 key 都用 readonly 关键字标注,这样就能获得每个 key 都是 readonly 只读的类型了。

Partial

/** * Make all properties in T optional */
type Partial<T> = {
    [P in keyof T]?: T[P];
};
复制代码

同理使用 ? 让每个 key 均可选。

type BlogPost = {
  id: number;
  title: string;
  description?: string;
}

type PartialBlogPost = Partial<BlogPost>;
/* => type PartialBlogPost { id?: number | undefined; title?: string / undefined; description?: string / undefined; } */
复制代码

Required

/** * Make all properties in T required */
type Required<T> = {
    [P in keyof T]-?: T[P];
};
复制代码

Required 类型和上两个差很少,须要注意的是 - 这个修饰符,它意味着去除后面的可选修饰符 ?, 那就是每个 key 都是必需。同理也存在 + 修饰符。

type BlogPost = {
  id: number;
  title: string;
  description?: string;
}

type RequiredBlogPost = Required<BlogPost>;
/* => type RequiredBlogPost { id: number; title: string; description: string; } */
复制代码

Record

在刚开始学习 TS 的时候,Record 的实现和使用场景让我比较困惑。

/** * Construct a type with a set of properties K of type T */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};
复制代码
type ExportFormat = "jsonMiniDiary" | "md" | "pdf" | "txtDayOne";
const fileExtensions: Record<ExportFormat, string> = {
	jsonMiniDiary: "json",
	md: "md",
	pdf: "pdf",
	txtDayOne: "txt",
};
复制代码

这是一段来自 real world 的代码。经过这段代码,能看到 Record 可以帮助咱们得到类型安全的对象声明。

待持续更新,TS 的类型世界真复杂。

相关文章
相关标签/搜索