Is there a difference between the type of new UnionSerializer([a,b])
and the type of new UnionSerializer([b,a])
? It doesn't really look like it, given that your type signatures for UnionSerializerConstructor
return a Serializer<A | B>
which would be the same as Serializer<B | A>
. In that case, good news! You don't need to worry about specifying an generic arbitrary-length tuple type, which you can't do yet. Instead, just accept an array:
interface UnionSerializerConstructor {
new <T>(types: T[]): Serializer<T>
}
If you pass in [a,b]
(where a
is an A
and b
is a B
) to the constructor, it will infer A | B
for T
, as you want.
Now, it looks like you expect the values passed into the constructor to themselves be Serializer
instances. In that case, you should probably do this instead:
interface UnionSerializerConstructor {
new <T>(types: (Serializer<T>)[]): Serializer<T>
}
I might as well go ahead and type your UnionSerializer
for you:
class UnionSerializer<T> implements Serializer<T> {
constructor(public types: (Serializer<T>)[]) {
// next line is unnecessary with "public types" above
// this.types = types
}
serialize(value: T): ArrayBuffer {
for (const type of this.types) {
try {
return type.serialize(value) //throws error if type can't serialize value
}
catch (e) { }
}
throw new Error('No type matched')
}
}
Let me know if you need me to explain any of that. Hope it helps. Good luck!
Update 1
@csander said:
Thanks for the answer! This was my original solution. The problem I have though is that not all the types I pass in will necessarily be Serializer<T>
with the same T
. For example, I might have a Serializer<string>
and a Serializer<number>
, so the desired T
for the union would be string | number
but neither of the original Serializer
s implement Serializer<string | number>
. Does that make sense?
That is an excellent point and I failed to address it. The short answer is that in TypeScript generic type parameters are covariant, meaning that, in fact, you can always narrow a Serializer<string|number>
value to one of type Serializer<string>
:
declare const numberOrStringSerializer: Serializer<number | string>;
const justStringSerializer: Serializer<string> = numberOrStringSerializer;
const justNumberSerializer: Serializer<number> = numberOrStringSerializer;
(This type of narrowing is unsound in general, but TypeScript does it for reasons of performance and developer convenience. There are possible fixes for it but they're not part of the language yet.)
That being said, type inference on the UnionSerializer
constructor will not automatically infer the union type:
new UnionSerializer([justNumberSerializer, justStringSerializer]); // error
You can manually specify it, however:
new UnionSerializer<string|number>([justNumberSerializer, justStringSerializer]); // ok
This is a bit annoying, but it works. There may be ways to improve this experience but I'm not sure at the moment. Does this help?