In a TypeScript project, I have an array of containers that carry a type property and some additional data, depending on their type.
type Container<Type extends string> = {
type: Type;
}
type AContainer = Container<"a"> & {
dataA: number;
}
type BContainer = Container<"b"> & {
dataB: boolean;
}
const data: (AContainer | BContainer)[] = [
{ type: "a", dataA: 17 },
{ type: "b", dataB: true }
];
My goal is to write a function that allows me to select an element from that array by its type, with full type safety. Something like this:
const getByType = <T extends string>(data: Container<string>[], type: T): Container<T> => {
for (const c of data) {
if (c.type === type) return c;
}
throw new Error(`No element of type ${type} found.`);
};
const dataA: AContainer = getByType(data, "a");
The problem is trying to convince TypeScript that the function is type-safe, and the return value is an element of the original array and has the requested type.
Here's my best attempt:
const getByType = <ContainerType extends Container<string>, Type extends string>(data: (ContainerType & Container<string>)[], type: Type): ContainerType & Container<Type> => {
for (const c of data) {
if (c.type === type) return c;
}
throw new Error(`No element of type ${type} found.`);
};
However, TypeScript neither understands that the comparison c.type === type ensures a Container<string> turns into a Container<Type>, nor that the return type of an example call, AContainer | (Container<"b"> & { dataB: boolean; } & Container<"a">), is equal to AContainer because of the conflict in Container<"b"> & Container<"a">.
The first problem can be solved by using a type predicate as the one in the following code block (although that kind of feels like cheating), but I have not found a solution for the second problem.
const isContainer = <Type extends string>(c: Container<string>, type: Type): c is Container<Type> => {
return typeof c === "object" && c.type === type;
};
Is there any way to get this to work? I'd prefer it if both getByType itself and its use were type-safe, but if that's not possible, I want at least the usage of getByType to not require any unsafe type assertions.
I can change the definitions of the container types, but the actual data is fixed. (For background: xml2js XML parser.)