你能做对这几道TypeScript练习题吗

·  阅读 490

前言

前段时间在 GitHub 上发现了一个 TypeScript 练习题的仓库:typescript-exercises,里面一共 15 道题,包含了 TS 的初级到高级的用法,我从这些题中挑选了几道,我们一起来看下,看看你能作对几道,正好可以巩固下知识,废话不多说,我们直接开始:

练习题

某些题目中的代码可能比较多,本文就不把这些全部贴出来了,我会在下面每个题目的后面写上该题目的 GitHub 地址,你可以在点进去查看详细的题目,建议将整个项目 clone 下来,在自己的电脑上查看。

exercises-7

GitHub 地址:exercises-7
题目
// index.ts
export function swap(v1, v2) {
  return [v2, v1]
}
复制代码
// test.ts
import { swap } from './index'

const pair1 = swap(123, 'hello') // 此时pair1的类型为any
typeAssert<IsTypeEqual<typeof pair1, [string, number]>>() // 这一行会报错因为any和[string, number]不等
复制代码
解答

从 test 中可以知道,我们需要使swap(123, 'hello')的返回结果的类型为[string, number],就是将入参反过来,可以先观察下 swap 函数,它的参数和返回值没有指定任何类型,这里我们可以使用泛型来完善它:

export function swap<T1, T2>(v1: T1, v2: T2): [T2, T1] {
  return [v2, v1]
}
复制代码

这样一来,TS 就会推断出swap(123, 'hello')的返回值类型是[string, number]了。

exercises-8

GitHub 地址:exercises-8
题目
// index.ts
interface User {
  type: 'user'
  name: string
  age: number
  occupation: string
}

interface Admin {
  type: 'admin'
  name: string
  age: number
  role: string
}

/*
  定义PowerUser类型,使它拥有User和Admin的所有字段,其中的type字段为 "powerUser"
*/
type PowerUser = unknown

export type Person = User | Admin | PowerUser
复制代码
解答

这里是需要我们完善 PowerUser 类型,使它拥有 User 和 Admin 的所有字段,并且它的 type 字段为 "powerUser",很显然,这里需要使用联合类型:

type PowerUser = User & Admin & { type: 'powerUser' } // 得到的类型为any
复制代码

如果这样写的话,你就会发现 PowerUser 类型变成了 any,这是为什么呢?观察 User 和 Admin 类型可以发现这两个类型中都含有 type 且 type 都为特定的字符串,这样的话我们直接用&操作符是有问题的,因为一个字符串不可能同时是'user''admin',那么该怎么办呢?其实只需要将 User 和 Admin 中的 type 去掉,然后再合并就可以了:

type PowerUser = Omit<User, 'type'> &
  Omit<Admin, 'type'> & {
    type: 'powerUser'
  }
复制代码

这样一来就达到题目的要求了,其中用了辅助泛型Omit

exercises-9

GitHub 地址:exercises-9
题目
// index.ts
/**
  这个题目太长了,比较占地方,大家就点击上方的链接查看吧,这里我只贴出来题目要求:
  删除UsersApiResponse和AdminsApiResponse类型,并使用通用类型ApiResponse来为每一个API函数指定响应的格式
*/
export type ApiResponse<T> = unknown
复制代码
解答

我们可以先看下题目中的响应数据的格式

type AdminsApiResponse =
  | {
      status: 'success'
      data: Admin[]
    }
  | {
      status: 'error'
      error: string
    }
type UsersApiResponse =
  | {
      status: 'success'
      data: User[]
    }
  | {
      status: 'error'
      error: string
    }
复制代码

每个响应数据都有成功和失败的状态,成功时返回的是 data,失败时返回的是 error,error 的类型都是 string,只有 data 不一样。

按照题目的要求,我们需要写一个通用的类型,这里我们可以用泛型来进行重构:

export type ApiResponse<T> =
  | { status: 'success'; data: T }
  | { status: 'error'; error: string }
复制代码

使用的时候只需要传入泛型就可以了:

export function requestAdmins(
  callback: (response: ApiResponse<Admin[]>) => void
) {
  callback({
    status: 'success',
    data: admins
  })
}
复制代码

其他函数同理,也是这样改造。

前面几道题都是比较简单的,下面我们看下稍微复杂点的题目。

exercises-10

GitHub 地址:exercises-10
题目
// index.ts
/**
具体题目点击上方链接可以看到,这里只贴出来题目要求:
  Exercise:
    我们不想重新实现所有的数据请求功能。
    让我们装饰一下旧的基于回调的函数,
    其结果与promise兼容。
    函数最终应返回一个promise,这个promise会resolve最终的数据或reject一个error,
    函数名为promisify。

    更高难度的练习:

    创建一个函数promisifyAll,
    它接受带有函数的对象,
    并返回一个新对象,
    其中每个函数都是promised的。

    const api = promisifyAll(oldApi);
*/
复制代码
// test.ts
复制代码
解答

这道题就是需要我们把旧的基于回调的 API 改成 promise,我们先来看下这些 API 的实现:

const oldApi = {
  requestAdmins(callback: (response: ApiResponse<Admin[]>) => void) {
    callback({
      status: 'success',
      data: admins
    })
  },
  requestUsers(callback: (response: ApiResponse<User[]>) => void) {
    callback({
      status: 'success',
      data: users
    })
  },
  requestCurrentServerTime(callback: (response: ApiResponse<number>) => void) {
    callback({
      status: 'success',
      data: Date.now()
    })
  },
  requestCoffeeMachineQueueLength(
    callback: (response: ApiResponse<number>) => void
  ) {
    callback({
      status: 'error',
      error: 'Numeric value has exceeded Number.MAX_SAFE_INTEGER.'
    })
  }
}

export const api = {
  requestAdmins: promisify(oldApi.requestAdmins),
  requestUsers: promisify(oldApi.requestUsers),
  requestCurrentServerTime: promisify(oldApi.requestCurrentServerTime),
  requestCoffeeMachineQueueLength: promisify(
    oldApi.requestCoffeeMachineQueueLength
  )
}
复制代码

需要实现一个 promisify 函数,这个函数接收一个旧的 api 函数,返回一个新的函数,这个新的函数是已经被 promise 包装好的函数,我们用 js 可以很轻易的实现这个 promisify:

export function promisify(fn) {
  return () =>
    new Promise((resolve, reject) => {
      fn((response) => {
        if (response.status === 'success') {
          resolve(response.data)
        } else {
          reject(new Error(response.error))
        }
      })
    })
}
复制代码

然后我们把它改成 TS 版的,使能够正确推导出类型。首先就是要确定 promisify 的参数 fn,这个 fn 的类型就是 oldApi 中的函数类型,只需要照着写就行了:

// 这个函数的参数是一个callback函数,callback的参数是response: ApiResponse<T>,泛型T是接口的返回值,如:Admin[]
type CallbackBasedAsyncFunction<T> = (
  callback: (response: ApiResponse<T>) => void
) => void
复制代码

然后再确定 promisify 的返回值,如上文所说,它的返回值应该是个新的函数,这个新的函数返回一个 promise:

type PromiseBasedAsyncFunction<T> = () => Promise<T>
复制代码

函数内部的逻辑比较简单,没什么需要改的地方,只有一个就是返回new Promise的时候记得加上泛型 T,最终我们 promisify 函数就是这样的:

export function promisify<T>(
  fn: CallbackBasedAsyncFunction<T>
): PromiseBasedAsyncFunction<T> {
  return () =>
    new Promise<T>((resolve, reject) => {
      fn((response) => {
        if (response.status === 'success') {
          resolve(response.data)
        } else {
          reject(new Error(response.error))
        }
      })
    })
}
复制代码

然后再试一下:

const requestAdmins = promisify(oldApi.requestAdmins)
复制代码

可以正确得到 requestAdmins 的返回值类型PromiseBasedAsyncFunction<Admin[]>

这道题还有个更高难度的练习,就是要实现一个 promisifyAll 函数,将对象中的所有函数都改为基于 promise 的。

要实现这个函数,首先我们要确定的是函数的参数和返回值是什么,其实参数就是oldAPi这个对象,类型就是 oldApi 的类型:

type OldApi = {
  requestAdmins(callback: (response: ApiResponse<Admin[]>) => void): void
  requestUsers(callback: (response: ApiResponse<User[]>) => void): void
  requestCurrentServerTime(
    callback: (response: ApiResponse<number>) => void
  ): void
  requestCoffeeMachineQueueLength(
    callback: (response: ApiResponse<number>) => void
  ): void
}
复制代码

我们肯定不能直接就把这个类型用在函数中,可以想办法简化一下,这里面的每一个函数除了 callback 的参数 response 不同,其余的都是相同的,那么我们可以把这个不同之处提取出来:

type ApiResponses = {
  requestAdmins: Admin[]
  requestUsers: User[]
  requestCurrentServerTime: number
  requestCoffeeMachineQueueLength: number
}

type SourceObject = {
  [K in keyof ApiResponses]: CallbackBasedAsyncFunction<ApiResponses[K]>
}
复制代码

这里用到了上一问题里创建的 CallbackBasedAsyncFunction 类型,其实 SourceObject 可以再进一步,把 ApiResponses 作为泛型:

type SourceObject<T> = { [K in keyof T]: CallbackBasedAsyncFunction<T[K]> }
复制代码

我们只需要传入 ApiResponses 就可以得到 OldApi:

type OldApi = SourceObject<ApiResponses>
复制代码

现在我们可以得到 promisifyAll:

function promisifyAll<T>(obj: SourceObject<T>): unknown {}
复制代码

这里有人可能会认为在使用时需要传入泛型 T,即:promisifyAll<ApiResponses>(oldApi),这里是不需要的,刚才的 ApiResponses 只是我们思考时的产物,你在使用时只需要promisifyAll(oldApi)就可以了,这是因为泛型其实是双向的,只要你传入了参数 oldApi,TS 就会根据这个 oldApi 自己推导出泛型 T 为 ApiResponses。

这里还需要注意的是 SourceObject 中对 T 做了遍历操作,其实这里我们默认了 T 为对象形式,那么为了不出现意外情况,需要再给 T 加上泛型约束:

function promisifyAll<T extends { [key: string]: any }>(
  obj: SourceObject<T>
): unknown {}
复制代码

然后还有返回值的类型,有了 SourceObject 的基础,返回值的类型就很好写了,可以使用上一问题中的 CallbackBasedAsyncFunction 类型:

type SourceObject<T> = { [K in keyof T]: CallbackBasedAsyncFunction<T[K]> }
type PromisifiedObject<T> = { [K in keyof T]: PromiseBasedAsyncFunction<T[K]> }
function promisifyAll<T extends { [key: string]: any }>(
  obj: SourceObject<T>
): PromisifiedObject<T> {}
复制代码

然后还有函数内部的逻辑,其实函数内部的逻辑是比较简单的,我们先来用 js 实现一版:

function promisifyAll(obj) {
  const result = {}
  for (const key of Object.keys(obj)) {
    result[key] = promisify(obj[key])
  }
  return result
}
复制代码

然后和之前的结合,改成 TS 版的,内部逻辑比较简单,就不分析了,只要最终返回值是PromisifiedObject<T>就可以:

type SourceObject<T> = { [K in keyof T]: CallbackBasedAsyncFunction<T[K]> }
type PromisifiedObject<T> = { [K in keyof T]: PromiseBasedAsyncFunction<T[K]> }
function promisifyAll<T extends { [key: string]: any }>(
  obj: SourceObject<T>
): PromisifiedObject<T> {
  const result = {} as PromisifiedObject<T>
  for (const key of Object.keys(obj) as (keyof T)[]) {
    result[key] = promisify(obj[key])
  }
  return result
}
复制代码

使用时:

export const api = promisifyAll(oldApi)
复制代码

可以正确拿到api的类型。

总结

这几道题你是否能成功解出来呢?解不出来也没关系,我们可以慢慢学。这里我推荐你把这个git仓库的代码拉下来,这里面从易到难共15道题,你可以从第一题最简单的开始,自己尝试下,相信在这个过程中你一定能学到一些知识。

分类:
前端
标签: