Issue
Let's say we have the following input JSON data:
{
"foo": 1,
"bar": [2],
"baz": [3],
}
Is it possible to convert this JSON to the following type:
{
foo: '1'
bar: MyList<['2']>
baz: MySet<['3']>
}
Based on the following data:
{
foo: new MyType<string>(),
bar: new MyType<MyList<string[]>>(),
baz: new MyType<MySet<string[]>>(),
}
The conversion from number
to string
of the number literals is just to indicate I plan to do some transformation there (though the actual types in my use case are more complex).
I can get it to work to some degree with hardcoding the container types, e.g. MyList
:
const input = {
foo: 1,
bar: [2],
baz: [3],
} as const;
type Input = typeof input;
class MyType<T> {}
type MyList<T> = Array<T>;
type MySet<T> = Set<T>;
const data = {
foo: new MyType<string>(),
bar: new MyType<MyList<string[]>>(),
baz: new MyType<MySet<string[]>>(),
};
type Data = typeof data;
type ToString<N extends number> = `${N}`;
const result = {} as {
[K in keyof Input]: [Input[K], Data[K]] extends [infer I, MyType<infer T>]
? T extends string
? I extends number
? ToString<I>
: never
: T extends MyList<string[]>
? I extends readonly number[]
? MyList<number[]>
: never
: T extends MySet<string[]>
? I extends readonly number[]
? MySet<number[]>
: never
: never
: never;
};
// const result: {
// readonly foo: "1";
// readonly bar: MyList<number[]>;
// readonly baz: MySet<number[]>;
// }
Would it be possible to retain the literal tuple and without hardcoding the container types?
With my current code, not hardcoding would just be a matter of (assuming safety is ensured through other means):
const result = {} as {
[K in keyof Input]: [Input[K], Data[K]] extends [infer I, MyType<infer T>]
? T extends string
? I extends number
? ToString<I>
: never
: T
: never;
};
// const result: {
// readonly foo: "1";
// readonly bar: MyList<number[]>;
// readonly baz: MySet<number[]>;
// }
If I could pass to MyType
a type-level function that got as its argument the literal, it would be easy to implement these container types. However I have no idea if this is at all possible in TypeScript or if there are some other ways around it to get the intended result.
Solution
Thanks to the helpful comment of @kelly pointing me towards a blog post explaining a trick to emulate higher kinded types to some degree, I managed to get it to work as intended.
The trick described in the blog post is the following:
interface IdFunction {
readonly input: unknown
readonly output: this['input']
}
type X = (IdFunction & { readonly input: 5 })['output']
// type X = 5
Using it, the type of the input can be given at a later point, making it act like a type-level function that still expects arguments.
The key insight is that this
refers to the final type (in this case IdFunction & { readonly input: 5 }
, that resolves to { readonly input: 5; readonly output: this['input'] }
), making it possible to have multiple subtypes, yet when accessing them through the output
property of the parent type, it will still provide the information given in the subtype.
Given the example provided in my question, here is a full example of this trick being applied:
const input = {
foo: 1,
bar: [2],
baz: [3],
} as const;
type Input = typeof input;
type NumberToString<N extends number> = `${N}`;
type NumbersToStrings<T extends readonly [...any[]]> = T extends readonly [infer Head, ...infer Tail]
? Head extends number
? [NumberToString<Head>, ...NumbersToStrings<Tail>]
: [...NumbersToStrings<Tail>]
: [];
class MappedType {
readonly A: unknown;
readonly B: unknown;
}
class MappedList extends MappedType {
readonly A: [...number[]];
readonly B: MyList<NumbersToStrings<this['A']>>;
}
class MappedSet extends MappedType {
readonly A: [...number[]];
readonly B: MySet<NumbersToStrings<this['A']>>;
}
class MyType<T> {}
type MyList<T> = Array<T>;
type MySet<T> = Set<T>;
const data = {
foo: new MyType<string>(),
bar: new MyType<MappedList>(),
baz: new MyType<MappedSet>(),
};
type Data = typeof data;
const result = {} as {
[K in keyof Input]: [Input[K], Data[K]] extends [infer I, MyType<infer T>]
? T extends string
? I extends number
? NumberToString<I>
: never
: T extends MappedType
? (T & { readonly A: I })['B']
: never
: never;
};
// const result: {
// readonly foo: "1";
// readonly bar: MyList<["2"]>;
// readonly baz: MySet<["3"]>;
// }
Answered By - Matthijs Steen
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.