支持 tuple/union 深度转换的 TypeScript 映射类型

TypeScript mapped type that deep transforms with tuple/union support

我正在尝试创建一个实现递归类型转换的通用映射类型。

非常感谢@jcalz 提供了 .

的优雅解决方案

(请注意,工单的范围不同,与此工单不重复)

如下所示,当前映射类型不支持元组或联合类型。

有没有办法支持联合类型并使规范通过?

游乐场准备就绪Playground Link

/**
 * Recursive type transformation. Support scalar, object, array, and tuple within original type.
 * @example
 * DeepReplace<Original, [From, To] | [Date, string] | ...>
 */
type DeepReplace<T, M extends [any, any]> = T extends M[0] ?
  Replacement<M, T>
  :
  {
    [P in keyof T]: T[P] extends M[0]
    ? Replacement<M, T[P]>
    : T[P] extends object
    ? DeepReplace<T[P], M>
    : T[P];
  }

type Replacement<M extends [any, any], T> =
  M extends any ? [T] extends [M[0]] ? M[1] : never : never;

// Tests

const obj = {
  number: 1,
  date: new Date(),
  deep: { date: new Date() },
  arrayDeep: [{ date: new Date() }],
  array: [new Date()],
  tuple: [new Date(), 2, true],
  tupleWithObj: [{ date: new Date() }, 2, 'hi', { hello: 'world' }],
  tupleWithTuple: [[1, false], [2, new Date()], [3, { date: new Date() }]]
}

type ArrayType<A extends unknown[]> = $ElementType<A, number>

const date = new Date()
const number = 2
const n = null
const nestedArray = [[[new Date()]]]

const scalarTest: DeepReplace<typeof date, [Date, string]> = 'string' // ✅
const constTest: DeepReplace<typeof number, [Date, string]> = 2 // ✅
const primitiveTest: DeepReplace<typeof n, [Date, string]> = null // ✅
const nestedArrayTest: DeepReplace<typeof nestedArray, [Date, string]> = [[['string']]] // ✅

let o: DeepReplace<typeof obj, [Date, string]>

const innocentTest: typeof o.number = 2 // ✅
const shallowTest: typeof o.date = 'string' // ✅
const deepTest: typeof o.deep.date = 'string' // ✅
const arrayTest: ArrayType<typeof o.array> = 'string' // ✅
const arrayObjTest: ArrayType<typeof o.arrayDeep>['date'] = 'string' // ✅
const tupleTest: typeof o.tuple = ['string'] // ❌ Type 'string' is not assignable to type 'number | boolean | Date'.
const tupleObjTest: typeof o.tupleWithObj = { date: 'string' } // ❌ Object literal may only specify known properties, and 'date' does not exist in type '(string | number | { date: Date; soHard?: undefined; } | { soHard: string; date?: undefined; })[]'
const tupleTupleTest: typeof o.tupleWithTuple = [[1, false], [2, 'string'], [3, { date: 'string' }]] // ❌ Type 'string' is not assignable to type 'number | boolean | Date | { date: Date; }'; Type 'string' is not assignable to type 'Date'.

有两个部分(需要两件事才能让它们工作)

联合类型

您需要使用 Extract and Exclude 实用程序类型

元组类型

您需要使用 infer keyword

合计:

/**
 * Recursive type transformation. Support scalar, object, array, and tuple as original type.
 * @example
 * DeepReplace<Original, [From, To] | [Date, string] | ...>
 */
type DeepReplace<T, M extends [any, any]> = T extends M[0] ?
  Replacement<M, T>
  :
  {
    [P in keyof T]: T[P] extends M[0]
    ? Replacement<M, T[P]>
    : T[P] extends (infer R)[] // Is this a Tuple or array
    ? DeepReplace<R, M>[] // Replace the type of the tuple/array
    : T[P] extends object
    ? DeepReplace<T[P], M>
    : Extract<T[P], M[0]> extends M[0] // Is this a union with the searched for type?
    ? UnionReplacement<M, T[P]> // Replace the union
    : T[P];
  }

type Replacement<M extends [any, any], T> =
  M extends any ? [T] extends [M[0]] ? M[1] : never : never;

type UnionReplacement<M extends [any, any], T> =
  DeepReplace<Extract<T, object>, M> // Replace all object types of the union
  | Exclude<T, M[0] | object> // Get all types that are not objects (handled above) or M[0] (handled below)
  | M[1]; // Direct Replacement of M[0]

Playground

Also for anyone reading this for converting objects, you still need to really convert them, this just changes the type for typescript and does not guarantee that you will get the correct object, YOU STILL NEED TO DO THE CONVERSION JS STYLE