In a recent project, I have lots of classes that are essentially named tuples, and I frequently find myself needing to convert between those object and tuples. Which means, I basically have a lot of functions like this:
const tupleToFoo = ([a, b, c]) => new Foo(a, b, c)
const fooToTuple = (foo) => [foo.a, foo.b, foo.c]
I probably should have just left it at that, but I couldn't help and wonder whether there is a more generic way to write this code. Here is what I came up with:
type AnyConstructor<T> = { new (...args: any[]): T }
type KeysOf<T> = ReadonlyArray<keyof T>
type Lookup<T, K> = K extends keyof T ? T[K] : never
type UnionFromInterface<T, K extends KeysOf<T>> = T[K[number]]
type TupleFromInterface<T, K extends KeysOf<T>> = {
[I in keyof K]: Lookup<T, K[I]>
}
function construct<T, K extends ReadonlyArray<keyof T>>(
C: AnyConstructor<T>
): (t: UnionFromInterface<T, K>[]) => T {
return t => new C(...t)
}
function deconstruct<T, K extends ReadonlyArray<keyof T>>(
D: K
): (o: T) => UnionFromInterface<T, K>[] {
return o => D.map(k => o[k])
}
function tupleToUnions<T, K extends ReadonlyArray<keyof T>>(
t: TupleFromInterface<T, K>
): UnionFromInterface<T, K>[] {
return t.map(i => i as UnionFromInterface<T, K>)
}
// This is particularly bad... :(
function unionsToTuple<T, K extends ReadonlyArray<keyof T>>(
u: UnionFromInterface<T, K>[]
): TupleFromInterface<T, K> {
return (u as unknown) as TupleFromInterface<T, K>
}
function instanceToTuple<T, K extends ReadonlyArray<keyof T>>(
T: (o: T) => UnionFromInterface<T, K>[]
): (o: T) => TupleFromInterface<T, K> {
return o => unionsToTuple(T(o))
}
function tupleToInstance<T, K extends ReadonlyArray<keyof T>>(
O: (t: UnionFromInterface<T, K>[]) => T
): (t: TupleFromInterface<T, K>) => T {
return t => O(tupleToUnions(t))
}
Which can be used as follows:
class Person {
static Keys = ['name', 'hobbies'] as const
name: string
hobbies?: string[]
constructor(name: string, hobbies?: string[]) {
this.name = name
this.hobbies = hobbies
}
}
const tupleToPerson = tupleToInstance(
construct<Person, typeof Person.Keys>(Person)
)
const personToTuple = instanceToTuple(
deconstruct<Person, typeof Person.Keys>(Person.Keys)
)
const p = new Person('John Doe', ['yodeling'])
const t = personToTuple(p)
const q = tupleToPerson(t)
console.log(p, t, q)
While that "works", the problem is that it is not at all type-safe. It feels like there should be a type-safe way of doing this, since all the necessary information is available at compile time. Hence, I was curious whether anybody knows a type-safe way to implement something like this?