Reputation: 8435
I am trying to create a universal mapped type that achieves recursive type transformation.
Huge thanks to @jcalz for the elegant solution from https://stackoverflow.com/a/60437613/1401634.
(Note that ticket was with a different scope, and is not duplicate with this ticket)
As shown below, the current mapped type does not support tuple or union type.
Is there a way to support union types and make the specs pass?
Playground ready 👉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'.
Upvotes: 3
Views: 535
Reputation: 121
Thanks Elias Schablowski for this great answer.
I stumbled across this Question and the previous one that led to it when searching for a type to expand matching types deeply. I was able to come up with something that worked well based on the overall structure of Elias's example and instead of replacing a type, just expanding it by unioning it with the other matching type. Maybe others here will find it useful.
type DeepAddUnion<T, M extends [unknown, unknown]> = T extends M[0]
? UnionWithMatchingTuplePartner<M, T>
: {
[P in keyof T]: T[P] extends M[0]
? UnionWithMatchingTuplePartner<M, T[P]>
: T[P] extends (infer R)[] // Is this a Tuple or array
? DeepAddUnion<R, M>[] // Handle the type of the tuple/array
: T[P] extends object
? DeepAddUnion<T[P], M>
: Extract<T[P], M[0]> extends M[0] // Is this a union with the searched for type?
? AddUnionToUnionedTypes<M, T[P]> // Add to the union
: T[P];
};
type UnionWithMatchingTuplePartner<
M extends [unknown, unknown],
T
> = M extends unknown ? ([T] extends [M[0]] ? M[0] | M[1] : never) : never;
type AddUnionToUnionedTypes<M extends [unknown, unknown], T> =
| DeepAddUnion<Extract<T, object>, M> // Handle all object types of the union
| Exclude<T, M[0] | object> // Keep all types that are not objects
| M[0] // Keep original type
| M[1]; // Add the matching tuple value
type AcceptEquivalents<T> = DeepAddUnion<
T,
[undefined, null] | [object, Prisma.JsonValue]
>;
(I'll use variables from the original solution given)
[T] extends [M[0]]
in Replacement
. Why do T
and M[0]
need to be wrapped in array (assuming that's what this is)?M
in Replacement
. Intuitively whats written makes sense, but I know the M
is actually a union of many tuples, I feel like I don't quite understand how M[1]
gets matched with the correct M[0]
or maybe it's that I don't understand how extending a union type is able to narrow that union type? 🤷♂️Apologies for asking a follow-on question from within an answer, I am 14 rep short of being able to comment 😵💫
Upvotes: 0
Reputation: 2812
There are two parts (and two things needed to get them working)
You would need to make use of the Extract
and Exclude
Utility types
You need to use the 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]
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
Upvotes: 3