Within a TypeScript project, there exists an array of containers that possess a type
attribute along with supplementary data based on their individual types.
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 }
];
The objective is to formulate a function that will permit the selection of an element from the aforementioned array by its designated type
while maintaining full adherence to type safety. An example implementation might look 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 complication arises in persuading TypeScript that the function adheres to strict type-safety guidelines and conclusively returns an element of the original array aligned with the requested type.
A potential solution has been attempted as depicted below:
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 encounters difficulty in comprehending that the comparison 'c.type === type
' substantiates a transformation from a Container<string>
structure into a Container<Type>
. Furthermore, it struggles to recognize that the return type derived from an exemplary call such as '
AContainer | (Container<"b"> & { dataB: boolean; } & Container<"a">)
' should ideally equate to AContainer
despite the clash between Container<"b">
and Container<"a">
.
A feasible workaround involves leveraging a type predicate akin to the one represented below which can resolve the initial concern but falls short in addressing the subsequent issue:
const isContainer = (c: Container<string>, type: Type): c is Container<Type> => {
return typeof c === "object" && c.type === type;
};
Is there a viable methodology to rectify this predicament? Ideally, both the implementation of getByType
itself and its utilization should exhibit rigorous adherence to type-safety principles. In scenarios where absolute compliance may not be attainable, the preference lies in ensuring that invoking getByType
does not necessitate any compromises through unsafe type assertions.
The definitions pertaining to the container types can be modified, although the factual data remains immutable. This setup originates from interactions with the xml2js XML parsing framework.