I have the following types:
type OrBranch = {
or: Branch[]
}
type AndBranch = {
and: Branch[]
}
I'd like a type Branch
that can be either an OrBranch
or an AndBranch
. So I first tried:
type Branch = AndBrand | OrBranch
Work great, unless I want to do something like:
let branch: Branch = ...
let andOr = 'and'; // or 'or'
let nodes = branch[andOr]
Then I get that branch
isn't indexable. OK, so I try an indexable type:
type AndOr = 'and' | 'or';
type Branch = Record<AndOr, Branch[]>;
But that requires that BOTH and
and or
exist, so I can't cast and AndBranch to Branch in this case.
Similarly
type Branch = Record<AndOr, Branch[]> | AndBranch | OrBranch
doesn't work, for the same reason.
While I could use type guards to determine the type, I have long functions that operate on these objects, in which they can be treated the same other than the property. I was hoping to eliminate a bunch of duplicate code by using the andOr
variable, which type guards don't really prevent. For example:
let retval = {} as Branch;
if (isAnd(branch)) { // branch is a Branch parameter passed in
(retval as AndBranch).and = [] as Branch[];
set = (retval as AndBranch).and;
} else {
(retval as OrBranch).or = [] as Branch[];
set = (retval as OrBranch).or;
}
set = _.reduce(set, (all, item: Branch)=> {
if (isAnd(branch) && isAnd(item))
return _.union(all, item.and);
else if (isOr(branch) && isOr(item))
return _.union(all, item.or);
else
return all;
}, [] as Branch[]);
vs.
andOr = isAnd(branch) ? 'and' : 'or';
let retval = {} as Branch;
retval[andOr] = _.reduce(set, (all, item: Branch) => {
if (item[andOr])
return _.union(all, item[andOr]);
else
return all;
}, [] as Branch[]);
I know there's a way to require exactly one of and
and or
(like the answer to Enforce Typescript object has exactly one key from a set). But that type is not indexable.
Is it possible to get both effects?
CodePudding user response:
Making the type indexable won't solve the basic problem, which is that you're trying to use the property and
on something that may be an and
branch (and thus have the property) or may be an or
branch (and thus not). Instead, ask the branch what it has and use that, because that allows TypeScript to narrow the type:
if ("and" in branch) {
// ...use `branch.and`...
} else {
// ...use `branch.or`...
}
You could combine that with your andOr
if you have code that thinks it knows what the branch is, perhaps by using a type assertion function:
function assertIsAndBranch(branch: Branch): asserts branch is AndBranch {
if (!("and" in branch)) {
throw new Error(`branch is not an AndBranch`);
}
}
// (And `assertIsOrBranch`)
Then:
if (andOr === "and") {
assertIsAndBranch(branch);
// ...here, TypeScript knows `branch` is an `AndBranch`...
}
Re your edit: If you have significant code that needs to deal with the items contained by AndBranch
's and
or OrBranch
's or
without knowing or caring whether they're AndBranch
or OrBranch
instances, I'd redesign them as a discriminated union where all members of the union have the same items
(or whatever) property:
type OrBranch = {
type: "or";
items: Branch[];
};
type AndBranch = {
type: "and";
items: Branch[];
};
That way, the code that doesn't care what type of branch it is can work with items
. The code you showed would be:
const items = _.reduce(branch.items, (all, item) => {
return _.union(all, item.items);
}, [] as Branch[]);
const retval = {type: branch.type, items};
Having properties do double-duty (both indicating the type of branch and the items within it) as they do in your current types makes writing typesafe code to handle the items without knowing/caring what type of branch it is really hard.