I have two types, that are unions of strings:
The NodeTypes
are used in our code to define the type of an viewmodel object
type NodeTypes =
| 'PRO_TERM'
| 'THEME'
| 'REFERENCE'
....
then we have ContentTypes which defines a list of possiblities for the ‘type’ field from our content coming out of the API
type ContentTypes =
| 'TERM'
| 'THEME'
| 'REFERENCE'
....
the strings I’ve used are just for example. the list is more than 100 values, each, and they have some overlap, but their use is very distinct.
The problem:
Let’s say I have an array of NodeTypes
const someItems: NodeTypes[] = ['PRO_TERM', 'THEME'];
const otherItems: ContentTypes[] = ['TERM', 'THEME', ...];
const test = someItems.includes(otherItems[0]); // <-- TS should throw an error
it happens sometimes that some comparisons are done, where we are accidentally comparing NodeTypes
with ContentTypes
, and since we use them just as strings, TS doesn’t error on that.
So, I’d like to brand each of the possibilities for both of these types, so the mix up is no longer possible.
I have the following:
const nodeTypes = [
'PRO_TERM',
'THEME',
'REFERENCE',
....
] as const;
// the union type of all the branded strings
export type NodeType = (typeof nodeTypes)[number] & { __brand: 'NodeType' };
// an enum-like object to use the branded types without having to use 'as'
export const NODETYPES = Object.fromEntries(nodeTypes.map((type) => [type, type])) as {
[K in (typeof nodeTypes)[number]]: K & { __brand: 'NodeType' };
};
// maps to
// NODETYPES: {
// PRO_TERM: "PRO_TERM" & {
// __brand: "NodeType";
// };
// THEME: "THEME" & {
// __brand: "NodeType";
// };
....
creating that same array now became:
const someItems: NodeTypes[] = [NODETYPES.PRO_TERM, NODETYPES.THEME];
since I didn’t want to do:
const someItems = ['PRO_TERM', 'THEME'] as NodeTypes[];
because then it would take any string as NodeTypes
this kinda works, but I did lose type inference which I did have before I started:
type ProTermNode = BasicNode & {
type: 'PRO_TERM'
}
type ContentNode = ProTermNode | ThemeNode | ....
const node: ContentNode = {...}
if (node.type === 'PRO_TERM') {
-- node is now inferred as ProTermNode
}
So now, when
type ProTermNode = BasicNode & {
type: typeof NODETYPES.PRO_TERM;
}
...
const node: ContentNode = {...}
if (node.type === NODETYPES.PRO_TERM) {
-- node is still ContentNode
}
This all leads me to believe there must be an easier/simpler way to do this, that perhaps also could fix my type inference issue.
what am I missing? how can this be improved?