Home > database >  Can you create a Typescript type that has exactly one of a set of properties AND is indexable by tha
Can you create a Typescript type that has exactly one of a set of properties AND is indexable by tha

Time:02-20

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};

Playground link

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.

  • Related