TypeScript 泛型从新手到入门

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

从一些简单的例子来学习类型体操,效率更高。

所有题目来自 type-challenges简单挑战。

题目解答同步到 github

泛型可以理解成类型函数。<T> 就表示类型参数,和形参数一样,<>里写什么字母都可以。

实现 Pick

4 - 实现 Pick
by Anthony Fu (@antfu) #简单 #union #built-in

题目

实现 TS 内置的 Pick<T, K>,但不可以使用它。

从类型 T 中选择出属性 K,构造成一个新的类型

例如:

interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyPick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
}
复制代码

知识点

  1. keyof
  • keyof 的作用将一个 类型 映射为它 所有成员名称的联合类型
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}
type todoKey = keyof Todo // "title" | "description" | "completed"
复制代码
  • 实际使用

比如我们经常获取一个对象的某个属性,会获取到 undefined

function getProperty(obj, key) {
  return obj[key];
}

const obj = {
  foo: 1,
  bar: 2,
  baz: 3,
};

const foo = getProperty(obj, "foo");
const b = getProperty(obj, "b");
console.log(b); // undefined
复制代码

这时,我们直接添加泛型,会报错。因为现在无法确定 K 是否是 T 的成员属性。

function getProperty<T, K>(obj: T, key: K) {
  return obj[key];  // Error 类型“K”无法用于索引类型“T”。
}
复制代码

keyof 来获取 T 的所有成员属性,用 extends 来判断 K 是否可以赋值它。

用上面的例子来分解:

// 这里泛型的意思就是,K 是 T 成员属性中的一个。如果 K 传的值不是 T 成员属性其中之一,就会报错。
function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

const obj = {
  foo: 1,
  bar: 2,
  baz: 3,
};

// keyof T => "foo" | "bar" | "baz"

const foo = getProperty(obj, "foo"); // "foo" extends "foo" | "bar" | "baz",正常

const b = getProperty(obj, "b");  // "b" extends "foo" | "bar" | "baz",报错
复制代码

image.png 2. extends

  • extends 有很多功能,这里只讲上面用到的功能。 判断是否能将左边的类型赋值给右边的类型
  • 实际使用
// 左边能赋值给右边
type trueType = "foo" extends "foo" | "bar" | "baz" ? "trueType" : "falseType"
// 左边不能赋值给右边
type falseType = "b" extends "foo" | "bar" | "baz" ? "trueType" : "falseType"
复制代码
  1. in
  • in 操作符用于遍历目标类型的公开属性名。类似 for .. in 的机制。

  • 实际使用

// 遍历枚举类型
enum Letter {
  A,
  B,
  C,
}
type LetterMap = { [key in Letter]: string };

// 遍历联合类型
type Property = "name" | "age" | "phoneNum";
type PropertyObject = { [key in Property]: string };
复制代码

解答

3 条测试用例

  1. MyPick<Todo, 'title' | 'completed' | 'invalid'> K extends keyof T,限制了 K 只能是 T 的成员属性,否则报错
  2. Expect<Equal<Expected1, MyPick<Todo, 'title'>>>
  3. Expect<Equal<Expected2, MyPick<Todo, 'title' | 'completed'>>>

2 和 3 都是 U 是否是 K 的一个属性,如果是就添加。就达到了 Pick 的效果了。

type MyPick<T, K extends keyof T> = {
  [U in K]: T[U]
}
复制代码

实现 Readonly

7 - 实现 Readonly
by Anthony Fu (@antfu) #简单 #built-in #readonly #object-keys

题目

不要使用内置的Readonly<T>,自己实现一个。

Readonly 会接收一个 泛型参数,并返回一个完全一样的类型,只是所有属性都会被 readonly 所修饰。

也就是不可以再对该对象的属性赋值。

例如:

interface Todo {
  title: string
  description: string
}

const todo: MyReadonly<Todo> = {
  title: "Hey",
  description: "foobar"
}

todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property
复制代码

知识点

  1. readonly
type Foo = {
  readonly bar: number;
  readonly bas: number;
};

// 初始化
const foo: Foo = { bar: 123, bas: 456 };

// 不能被改变
foo.bar = 456; // Error: foo.bar 为只读属性
复制代码

解答

这个用上面的 inkeyof 知识点就可以,遍历之后给所有属性加上 readonly

type MyReadonly<T> = {
  readonly [P in keyof T]: T[P]
}
复制代码

元组转换为对象

11 - 元组转换为对象
by sinoon (@sinoon) #简单

题目

传入一个元组类型,将这个元组类型转换为对象类型,这个对象类型的键/值都是从元组中遍历出来。

例如:

const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const

type result = TupleToObject<typeof tuple> // expected { tesla: 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}
复制代码

知识点

  1. const

字面量,可以理解为不可修改。

let a1 = '123'
type a1 = typeof a1  // string
a1 = "321"

const a2 = '123'
type a2 = typeof a2   // '123'
a2 = "321"  // 无法分配到 "a2" ,因为它是常数。
复制代码

image.png

数组同样,不加 const 就是可以修改,加 const 不可以修改,在 TS 里叫作 元组

let arr1 = ['tesla', 'model 3', 'model X', 'model Y']
arr1[0] = 'test'

const arr2 = ['tesla', 'model 3', 'model X', 'model Y'] as const
arr2[0] = 'test' // 无法分配到 "0" ,因为它是只读属性。

// as 是断言,意思是告诉 TS 服务,它就是 const
复制代码
  1. typeof

TS 里可以理解为有两部分,一部分是原来的 JS,一部分是类型。如果要获取一个JavaScript变量的类型就可以用 typeof

const add = (a: number, b: number): number => {
  return a + b
}
const obj = {
  name: 'AAA',
  age: 23
}

// 结果:(a: number, b:number) => number
type t1  = typeof add
// 结果:{ name: string; age: number; }
type t2 = typeof obj
复制代码

有些简单的判断,系统可以自动推断类型,所以不用 typeof 也可以有类型约束。但复杂的泛型不行。

  1. T[number]

表示返回所有数字型索引的元素,形成一个联合类型,例如:

const arr2 = ["tesla", "model 3", "model X", "model Y"] as const;
type Tuple = typeof arr2
type arr = Tuple[number];  // "tesla" | "model 3" | "model X" | "model Y"
复制代码

解答

还和上面一样,只不过需要先用T[number]获取到元组的所有元素,
然后遍历,
因为是获取到的元素,返回值直接返回 P 就行。

type TupleToObject<T extends readonly string[]> = {
  [P in T[number]]: P
}
复制代码

注意泛型里的 T extends readonly string[],如果使用 any 会导致 @ts-expect-error 这条不通过。

@ts-expect-error,如果我们把这个注释放在代码行前面,TypeScript 就会预期下面的代码会报错。就是报错才正常,如果不报错那这行注释会提示错误。

第一个元素

14 - 第一个元素
by Anthony Fu (@antfu) #简单 #array

题目

实现一个通用First<T>,它接受一个数组T并返回它的第一个元素的类型。

例如:

type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]

type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3
复制代码

知识点

  1. infer

extends 语句中,支持infer关键字,表示语句中待推断的类型变量。这个有点难以理解,还是看例子吧。

// 如果泛型变量 T 是 `() => infer R` 的子集,那么返回通过 infer 获取到的函数返回值,否则返回 boolean 类型。
type Func<T> = T extends () => infer R ? R : boolean;

let func1: Func<number>; // => boolean
let func2: Func<''>; // => boolean
let func3: Func<() => Promise<number>>; // => Promise<number>
复制代码
// T 如果可以赋值给 [infer H, ...any[]],就返回数组的第一个类型。
type arrHead<T> = T extends [infer H, ...any[]] ? H : never;

type arr = ["one", "two", "three"];

type head = arrHead<arr>; // "one"
复制代码

解答

  1. 利用 infer 获取第一个元素
type First<T extends any[]> = T extends [infer First, ...infer Rest] ? First : never
复制代码
  1. 利用 0 下标直接获取,判断一下是不是空数组。
type First<T extends any[]> = T extends [] ? never : T[0]
复制代码
  1. 利用 T['length'] 返回数组长度判断是否是空数组,然后用 0 下标获取第一个元素。
type First<T extends any[]> = T['length'] extends 0 ? never : T[0]
复制代码
  1. 利用之前的知识点 T[number] 返回所有元素的联合类型。
// T[number] T 为空时会返回 never
type First<T extends any[]> = T[0] extends T[number] ? T[0] : never
复制代码

这里需要注意下,First<[3, 2, 1]>,泛型里的所有参数都是类型而不是 JavaScript 对象。所以如果要从 JS 里引用的话,需要用 typeof。

获取元组长度

18 - 获取元组长度
by sinoon (@sinoon) #简单 #tuple

题目

创建一个通用的Length,接受一个readonly的数组,返回这个数组的长度。

例如:

type tesla = ['tesla', 'model 3', 'model X', 'model Y']
type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT']

type teslaLength = Length<tesla> // expected 4
type spaceXLength = Length<spaceX> // expected 5
复制代码

解答

这个没啥说的,知识点都是上面的。在泛型传入时约束下必须传入元组就行,否则 test case 过不了

type Length<T extends readonly unknown[]> = T['length']
复制代码

Exclude

43 - Exclude
by Zheeeng (@zheeeng) #简单 #built-in

题目

实现 TS 内置的 Exclude<T, K>,但不可以使用它。

// 结果:'name'|'age'
type ExcludeResult = Exclude<'name'|'age'|'sex', 'sex'|'address'>
复制代码

知识点

  1. extends

extends 的另一个特性。分配式,当 extends 左边类型是一个联合类型时,会进行拆分,有点类似数学中的分解因式:

type Diff<T, U> = T extends U ? never : T; // 找出T的差集
type Filter<T, U> = T extends U ? T : never; // 找出交集

type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">; // => "b" | "d"
// <"a" | "b" | "c" | "d", "a" | "c" | "f"> 相当于
/*
* <'a', "a" | "c" | "f"> |
* <'b', "a" | "c" | "f"> |
* <'c', "a" | "c" | "f"> |
* <'d', "a" | "c" | "f">
*/
type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">; // => "a" | "c"
// <"a" | "b" | "c" | "d", "a" | "c" | "f"> 同上

let demo1: Diff<number, string>; // => number
复制代码

解答

type MyExclude<T, U> = T extends U ? never : T
复制代码

Awaited

189 - Awaited
by Maciej Sikora (@maciejsikora) #简单 #promise #built-in

题目

假如我们有一个 Promise 对象,这个 Promise 对象会返回一个类型。在 TS 中,我们用 Promise 中的 T 来描述这个 Promise 返回的类型。请你实现一个类型,可以获取这个类型。

比如:Promise<ExampleType>,请你返回 ExampleType 类型。

这个挑战来自于 @maciejsikora 的文章:original article

解答

没啥说的,知识点都是上面的。传入值需要约束为 Promise,返回值需要递归判断 Promise 的泛型参数是否 Promise。

type MyAwaited<T extends Promise<any>> = T extends Promise<infer P>
  ? P extends Promise<any>
    ? MyAwaited<P>
    : P
  : T;
复制代码

If

268 - If
by Pavel Glushkov (@pashutk) #简单 #utils

题目

实现一个 IF 类型,它接收一个条件类型 C ,一个判断为真时的返回类型 T ,以及一个判断为假时的返回类型 FC 只能是 true 或者 falseTF 可以是任意类型。

举例:

type A = If<true, 'a', 'b'>  // expected to be 'a'
type B = If<false, 'a', 'b'> // expected to be 'b'
复制代码

解答

这题本身很简单,注意约束一下 C 的传入类型就行。

type If<C extends boolean, T, F> = C extends true ? T : F;
复制代码

关键在于 ts 配置,在不同的环境下,C extends boolean 有时 boolean 包含 null 有时不包含。

tsconfig.json 中开启 "strictNullChecks": true

interface User {
  name: string;
  age?: number;
}
function printUserInfo(user: User) {
  console.log(`${user.name}, ${user.age.toString()}`)
  // => error TS2532: Object is possibly 'undefined'.
  
  console.log(`${user.name}, ${user.age!.toString()}`)
  // => 使用 ! 断言,告诉 TS 你已经确认 user.age 是非空的。

  if (user.age != null) {
    console.log(`${user.name}, ${user.age.toString()}`)
  }
  // => 或者使用类型守卫,用 if 条件检查 user.age 是非空的。
复制代码

建议在所有情况下都开启 null 的严格模式检查。

Concat

533 - Concat
by Andrey Krasovsky (@bre30kra69cs) #简单 #array

题目

在类型系统里实现 JavaScript 内置的 Array.concat 方法,这个类型接受两个参数,返回的新数组类型应该按照输入参数从左到右的顺序合并为一个新的数组。

举例,

type Result = Concat<[1], [2]> // expected to be [1, 2]
复制代码

解答

这个没啥说的,用 JS 的思路,使用展开运算符,然后看报错添加约束。

type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U];
复制代码

Includes

898 - Includes
by null (@kynefuk) #简单 #array

题目

在类型系统里实现 JavaScript 的 Array.includes 方法,这个类型接受两个参数,返回的类型要么是 true 要么是 false

举例来说,

type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`
复制代码

解答

这道题在简单里有点复杂了。一般复杂的泛型最好先用 JS 来写一个思路,然后再转换成 TS 泛型。

function Includes(arr, key) {
  function helper(arr, key) {
    const [head, ...tail] = arr;
    if (head === key) {
      return true;
    } else {
      return tail.length === 0 ? false : helper(tail, key);
    }
  }
  helper(arr, key);
}
复制代码
// 使用 `@type-challenges/utils` 里的 `Equal`
import type { Equal, Expect } from '@type-challenges/utils'

type Includes<T extends readonly any[], U> = T extends [infer Head, ...infer Rest]
  ? Equal<Head, U> extends true ? true : Includes<Rest, U>
  : false
复制代码

或者自己写一个 Equal

type Equal1<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2)
  ? true
  : false;
复制代码

Push

3057 - Push
by jiangshan (@jiangshanmeta) #简单 #array

题目

在类型系统里实现通用的 Array.push

举例如下,

type Result = Push<[1, 2], '3'> // [1, 2, '3']
复制代码

解答

又恢复到很简单的,没啥说的。

type Push<T extends unknown[], U> = [...T, U]
复制代码

Unshift

3060 - Unshift
by jiangshan (@jiangshanmeta) #简单 #array

题目

实现类型版本的 Array.unshift

举例,

type Result = Unshift<[1, 2], 0> // [0, 1, 2,]
复制代码

解答

送分题,同上 。

type Unshift<T extends unknown[], U> = [U, ...T]
复制代码

Parameters

3312 - Parameters
by midorizemi (@midorizemi) #简单 #infer #tuple #built-in

题目

实现内置的 Parameters 类型,而不是直接使用它,可参考TypeScript官方文档

解答

算是熟悉 infer 的适用吧。

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

以上就是本篇文章的全部内容啦,感谢观看。

猜你喜欢

转载自juejin.im/post/7103046028235898888