0

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?

2
  • 1
    Does this work any better for you in general? If you like it I can write it up as an answer. You'll never get full type safety for such a dramatic transformation, but you can keep the unsafe stuff inside your converter implementation and users don't have to deal with it. Commented May 1, 2020 at 2:06
  • @jcalz That's quite cool, thank you for the comment! Maybe it could work, let me play around with it for a bit. As an aside, I have to admit that working on this I took a fair bit of inspiration from your answer here: stackoverflow.com/questions/55522477/… Commented May 1, 2020 at 3:24

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.