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.)