3

I have an array of type object[] & Tree[] but arr.map(child => ...) infers the type of the child as object rather than object & Tree.

Is there any way to avoid this without extra casting?

Notably Tree extends object but typescript doesn't seem to realise this and merge the two parts of the intersection type.

EDIT - Minimal reproducible example:

This is contrived, but based off my other recent question Transform a typescript object type to a mapped type that references itself

interface BasicInterface {
    name: string;
    children: object[];
}

function getBasic<T extends object>(input: T): BasicInterface {
    return input as BasicInterface;
}

export const basicTree = getBasic({
    name: 'parent',
    children: [{
        name: 'child',
        children: []
    }]
});

The point is the code below has access to "basicTree" and its inferred type. For this example I have defined BasicInterface, but in practice this is generated automatically and I haven't found a way to programatically generate a recursive interface.

I would like to add the recursive type of children back as the original interface defines.

Rather than completely redefine BasicInterface in the code as this could be a lot of boilerplate, I am trying to "enhance" the definition of the basicTree's type with the correct recursive definition.

But this falls down when getting the children's type. Perhaps there is a simpler solution?

type RestoredInterface = typeof basicTree & {
    children: RestoredInterface[]
};

function getTree(basic: BasicInterface): RestoredInterface {
    return basic as RestoredInterface;
}

const restoredTree = getTree(basicTree);

const names = restoredTree.children.map(child => child.name);
Hugheth
  • 1,802
  • 17
  • 14
  • 3
    intersections of functions are interpreted as [overloads](https://www.typescriptlang.org/docs/handbook/functions.html#overloads), so the call to `arr.map()` is likely considered calling the first overload; i.e., the `object[]`'s `map` method. I'm wondering why the type isn't `(object & Tree)[]` or `Tree[]`? Where's the intersection happening? Could you provide a [mcve] so someone can advise you? – jcalz Feb 19 '20 at 17:21
  • Ah I see thankyou. I will update the original question with an example for clarity – Hugheth Feb 19 '20 at 20:36
  • The inferred type of `basicTree` is just `BasicInterface`, and your intersection doesn't result in anything like `object[] & Tree[]`; instead it's just `object[] & OtherThingsThatAreNotArrays`. Are you sure this example reproduces your issue? – jcalz Feb 19 '20 at 20:46
  • `interface RestoredInterface extends BasicInterface { children: RestoredInterface[] }` might do what you want? Note that I think there's a typo in your example above since you have `RestoredInterface` in a place I expect to see `RestoredInterface[]`. – jcalz Feb 19 '20 at 20:52
  • Updated to include your correction thanks. The example is meant to demonstrate that I'm struggling to see how to combine two object interfaces `A` and `B` so that a child array called 'children' has type `(ElementOf & ElementOf)[]` rather than `ElementOf[] & ElementOf`. Maybe I need to use some recursive merge operation rather than a intersection – Hugheth Feb 19 '20 at 23:08
  • 1
    In the above, have you tried changing your definition of `RestoredInterface` to `interface RestoredInterface extends BasicInterface { children: RestoredInterface[] }`? That should fix the problem. If that doesn't meet your needs, please edit the example code to reflect this. – jcalz Feb 20 '20 at 01:25
  • The example is contrived - in practice, BasicInterface is programatically generated and the code below only gets basicTree as an object - so the question is how can you "extend" an interface without having reference to its definition – Hugheth Feb 20 '20 at 07:30
  • @jcalz see my answer. let me know the correct way to credit you – Hugheth Feb 20 '20 at 07:37

2 Answers2

1

I found a weird solution. Just change the order in the declaration. That's weird, but it works:

type RestoredInterface = {
    children: RestoredInterface[]
} & typeof basicTree;

Edit: Here is an explanation.

A better solution can be something like this:

type RestoredInterface = Omit<typeof basicTree, 'children'> & {
    children: RestoredInterface[]
};

See Playground

Shalom Peles
  • 2,285
  • 8
  • 21
  • Thanks, this works well. I've also proposed a solution, but this is the only appropriate solution when `typeof basicTree` is more complex e.g. if it is a union type itself, it doesn't have statically known keys and extends can't be used – Hugheth Feb 20 '20 at 09:32
0

Based off an observation from jcalz, it is actually possible to simply extend the original interface. I was confused because it is programatically defined, but that's not a problem if you name it first:

type BasicTreeInterface = typeof basicTree;

interface RestoredInterface extends BasicTreeInterface {
    children: RestoredInterface[]
};

const restoredTree = getTree(basicTree);

const names = restoredTree.children.map(child => {
    // Child is of type RestoredInterface
});
Hugheth
  • 1,802
  • 17
  • 14