typescript 具有通用模式支持的转换函数的正确类型

xzlaal3s  于 2023-04-22  发布在  TypeScript
关注(0)|答案(1)|浏览(129)

首先,场景:我是一个需要从一些REST API中检索数据的客户端,但是它们返回的json遵循了一个非常规的命名法(字段名称只有一个字符),我希望自动将响应Map到一个更显式的接口。
我提出了一个基于convert函数的解决方案,该函数接受我用作目标模式的mapping对象的原始响应。
这个解决方案甚至可以工作,但是我在尝试以通用(和正确)的方式设置类型时遇到了麻烦。目前我只是放了很多any,但我想了解函数和Map对象是什么类型。
(操场在此)
理想情况下,我希望将convert定义为泛型函数<T,U>,将obj定义为T,返回值为U,但我不知道如何正确地键入mapping,其类型依赖于TU

  1. interface AB {
  2. a: string;
  3. b: C;
  4. }
  5. interface C {
  6. c: number;
  7. }
  8. interface XY {
  9. x: string;
  10. y: Z;
  11. }
  12. interface Z {
  13. z: number;
  14. }
  15. const abc: AB = {
  16. a: '...',
  17. b: {
  18. c: 123,
  19. },
  20. };
  21. const ab_xy = {
  22. a: 'x',
  23. b: ['y', {
  24. c: 'z'
  25. }]
  26. };
  27. const convert = (obj: any, mapping: {}): any => {
  28. const ret: any = {};
  29. for (const [key_orig, value] of Object.entries(mapping)) {
  30. const value_orig = obj[key_orig];
  31. if (Array.isArray(value)) {// nested
  32. const [key_new, submapping] = value;
  33. ret[key_new] = convert(value_orig, submapping);
  34. }
  35. else {
  36. const key_new = value as any;
  37. ret[key_new] = value_orig;
  38. }
  39. }
  40. return ret;
  41. };
  42. const expected_xyz: XY = {
  43. x: '...',
  44. y: {
  45. z: 123,
  46. }
  47. };
  48. const actual_xyz = convert(abc, ab_xy);
  49. console.log(' abc: %o', abc);
  50. console.log('expected_xyz: %o', expected_xyz);
  51. console.log(' actual_xyz: %o', actual_xyz);
qyswt5oh

qyswt5oh1#

Playground:https://tsplay.dev/wOlQdN

  1. // key mapping type: a recursive type
  2. // which maps 'key' to 'newKey' for renaming
  3. // or to ['newKey', Mapping] for deeper renaming
  4. type Mapping<T> = {
  5. [K in keyof T]?: string | (
  6. T[K] extends Record<any, any> ? readonly [string, Mapping<T[K]>]
  7. : never
  8. )
  9. }
  10. // result of mapping
  11. type Mapped<T, M extends Mapping<T>> = {
  12. [K in keyof M & keyof T // take keys
  13. as ( // and rename them
  14. | M[K] extends string ? M[K] // if mapping is string, to that string
  15. : M[K] extends readonly [infer P extends string, any] ? P // if mapping is tuple, to its first element
  16. : never
  17. )]: (
  18. | M[K] extends string ? T[K] // if mapping is string, keep original value
  19. : M[K] extends readonly [any, infer P extends Mapping<T[K]>] ? Mapped<T[K], P> // otherwise map it using 2nd tuple item
  20. : never
  21. )
  22. }
  23. // unwraps inferred types display to avoid intellisence saying `Mapped<T, M>` and to say `{ b: 1 }`
  24. export type Debug<T> = T extends Record<any, any> ? { [K in keyof T]: Debug<T[K]> } : T
  25. // advanced typings to reduce amount of `any` and `as any`. Feel free to use just `Object.fn` instead.
  26. const recordEntries = Object.entries as <T>(o: T) => { [K in keyof T]: [K, T[K]] }[keyof T][]
  27. const fromEntries = Object.fromEntries as <T extends readonly (readonly [any, any])[]>(a: T) => { [K in T[number][0]]: Extract<T[number], [K, any]>[1] }
  28. function convert<T extends Record<string, any>, M extends Mapping<T>>(obj: T, mapping: M): Debug<Mapped<T, M>> {
  29. let entries = recordEntries(obj) // take [key, value] pairs
  30. let mappedEntries =
  31. entries.map(([key, value]) => { // map them
  32. let mapped = mapping[key]!
  33. if (typeof mapped === 'string') return [mapped, value] as const // changing key to mapping if it's a string
  34. else return [mapped[0], convert(value, mapped[1])] as const // changing key to mapping[0] and value to converted if it's a tuple
  35. })
  36. return fromEntries(mappedEntries) as any // the type for generic case can't be inferred, thus `as any`
  37. };
  38. let test1 = convert(
  39. // ^?
  40. // let test1: { b: 1; }
  41. { a: 1 } as const,
  42. { a: 'b' } as const
  43. )
  44. console.log(test1)
  45. let test2 = convert(
  46. // ^?
  47. // let test2: { aaa: { bbb: { ccc: { readonly d: 123; }; }; }; }
  48. { a: { b: { c: { d: 123 } } } } as const,
  49. { a: ['aaa', { b: ['bbb', { c: 'ccc' }] }] } as const
  50. )
  51. console.log(test2)
展开查看全部

相关问题