ts类型挑战【十二】

142 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第14天,点击查看活动详情

题目十五:deep-readonly

// template.ts
type DeepReadonly<T> = any
// test-cases.ts
import { Equal, Expect } from '@type-challenges/utils'

type cases = [
  Expect<Equal<DeepReadonly<X>, Expected>>,
]

type X = {
  a: () => 22
  b: string
  c: {
    d: boolean
    e: {
      g: {
        h: {
          i: true
          j: 'string'
        }
        k: 'hello'
      },
      l: ['hi']
    }
  }
}

type Expected = {
  readonly a: () => 22
  readonly b: string
  readonly c: {
    readonly d: boolean
    readonly e: {
      readonly g: {
        readonly h: {
          readonly i: true
          readonly j: 'string'
        }
        readonly k: 'hello'
      },
      readonly l: ['hi']
    }
  }
}

根据题目和测试用例我们可以知道,这里要实现一个深度递归

Record

在解题之前,我们先来了解一下 ts 内置方法 Record

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

将K中的每个属性([P in K]),都转为T类型。

type t1 = Record<string, unknown> // type t1 = { [x: string]: unknown; }

Record<K,T>构造具有给定类型T的一组属性K的类型。在将一个类型的属性映射到另一个类型的属性时,Record非常方便。

他会将一个类型的所有属性值都映射到另一个类型上并创造一个新的类型.

interface User {
  name: string
  age: number
}

let user: Record<number, User> = {
  0: { name: "hay0", age: 18 },
  1: { name: "hay1", age: 20 }
}

代码实现

  • 原代码
type DeepReadonly<T> = any
  • 遍历
type DeepReadonly<T> = {
  [P in keyof T]: T[P]
}
  • 添加 readonly 修饰符
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P]
}
  • 判断是否是对象,是对象则进行递归,不是则直接返回
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
  	? DeepReadonly<T[P]>
  	: T[P]
}

测试用例依然无法通过,这时我们就需要用到 Record 这个方法了,将 object 替换为 keystringvalueunknown 的方式

  • Record
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends Record<string, unknown>
  	? DeepReadonly<T[P]>
  	: T[P]
}

题目十六:chainable-options

// template.ts
type Chainable = {
  option(key: string, value: any): any
  get(): any
}
// test-cases.ts
import { Alike, Expect } from '@type-challenges/utils'

declare const a: Chainable

const result1 = a
  .option('foo', 123)
  .option('bar', { value: 'Hello World' })
  .option('name', 'type-challenges')
  .get()

const result2 = a
  .option('name', 'another name')
  // @ts-expect-error
  .option('name', 'last name')
  .get()

type cases = [
  Expect<Alike<typeof result1, Expected1>>,
  Expect<Alike<typeof result2, Expected2>>,
]

type Expected1 = {
  foo: number
  bar: {
    value: string
  }
  name: string
}

type Expected2 = {
  name: string
}

可串联构造器

在 JavaScript 中我们很常会使用可串联(Chainable/Pipeline)的函数构造一个对象,但在 TypeScript 中,你能合理的给他附上类型吗?

在这个挑战中,你需要提供两个函数 option(key, value)get()。在 option 中你需要使用提供的 key 和 value 扩展当前的对象类型,通过 get 获取最终结果。

可以假设 key 只接受字符串而 value 接受任何类型,你只需要暴露它传递的类型而不需要进行任何处理。同样的 key 只会被使用一次。

测试用例

  • 示例1
const result1 = a
  .option('foo', 123)
  .option('bar', { value: 'Hello World' })
  .option('name', 'type-challenges')
  .get()

最终的结果期望是:

type Expected1 = {
  foo: number
  bar: {
    value: string
  }
  name: string
}

相当于用链式的方式,将参数注入相应的对象

  • 示例2
const result2 = a
  .option('name', 'another name')
  // @ts-expect-error
  .option('name', 'last name')
  .get()

期望结果:

type Expected2 = {
  name: string
}

相同 key 的内容再次注入会报错

代码实现

  • 原代码
type Chainable = {
  option(key: string, value: any): any
  get(): any
}
  • 传入默认值(对象)
type Chainable<T = {}> = {
  option(key: string, value: any): any
  get(): any
}
  • key 需要是一个字符串
type Chainable<T = {}> = {
  option<K extends: string>(key: K, value: any): any
  get(): any
}
  • value 值随意
type Chainable<T = {}> = {
  option<K extends: string, V>(key: K, value: V): any
  get(): any
}
  • option 的返回值是一个 Chainable,在类型 T 的基础上添加传入 option 的类型
type Chainable<T = {}> = {
  option<K extends: string, V>(key: K, value: V): Chainable<T & Record<K, V>>
  get(): any
}
  • get 直接返回现有类型 T
type Chainable<T = {}> = {
  option<K extends: string, V>(key: K, value: V): Chainable<T & Record<K, V>>
  get(): T
}
  • 相同 key 报错

利用 extends 来判断 K 是否继承 T,如果继承则返回 never,否则可以使用 K(返回 K

type Chainable<T = {}> = {
  option<K extends: string, V>(key: K extends T ? never : K, value: V): Chainable<T & Record<K, V>>
  get(): T
}