Reputation: 5864
I use d3
with types definition from @types/d3
. I have a method that operates with the union of Selection and Transition. The argument could be either one. Both Selection and Transition types have the selectAll
method.
But when I try to apply selectAll
to the union, I get the following error:
This expression is not callable. Each member of the union type '{ (): Selection<null, undefined, BaseType, unknown>; (selector: null): Selection<null, undefined, BaseType, unknown>; (selector: undefined): Selection<...>; <DescElement extends BaseType, OldDatum>(selector: string): Selection<...>; <DescElement extends BaseType, OldDatum>(selector: ValueFn<...>): Selection<...>; } ...' has signatures, but none of those signatures are compatible with each other.
Here is an example of code that gives this error:
/* Definitions from @types/d3 */
export type BaseType = Element | Document | Window | null;
export type ValueFn<T extends BaseType, Datum, Result> = (this: T, datum: Datum, index: number, groups: T[] | ArrayLike<T>) => Result;
export interface Transition<GElement extends BaseType, Datum, PElement extends BaseType, PDatum> {
selectAll<DescElement extends BaseType, OldDatum>(selector: string): Transition<DescElement, OldDatum, GElement, Datum>;
selectAll<DescElement extends BaseType, OldDatum>(selector: ValueFn<GElement, Datum, DescElement[] | ArrayLike<DescElement>>): Transition<DescElement, OldDatum, GElement, Datum>;
}
export interface Selection<GElement extends BaseType, Datum, PElement extends BaseType, PDatum> {
selectAll(): Selection<null, undefined, GElement, Datum>;
selectAll(selector: null): Selection<null, undefined, GElement, Datum>;
selectAll(selector: undefined): Selection<null, undefined, GElement, Datum>;
selectAll<DescElement extends BaseType, OldDatum>(selector: string): Selection<DescElement, OldDatum, GElement, Datum>;
selectAll<DescElement extends BaseType, OldDatum>(selector: ValueFn<GElement, Datum, DescElement[] | ArrayLike<DescElement>>): Selection<DescElement, OldDatum, GElement, Datum>;
}
/* My code */
function myFunc(maybeTransition: Selection<BaseType, unknown, BaseType, unknown> | Transition<BaseType, unknown, BaseType, unknown>) {
const texts = maybeTransition.selectAll('text'); // ERROR: This expression is not callable.
// do something with texts
}
So the questions are:
Upvotes: 5
Views: 685
Reputation: 21578
For your function's parameter maybeTransition
you are specifying a union type expecting it to be exactly that, a union of both types:
Selection<BaseType, unknown, BaseType, unknown> | Transition<BaseType, unknown, BaseType, unknown>
Unfortunately and totally counter-intuitively, a union type behaves quite differently from what you expect! The documentation tells us that
A union type is a type formed from two or more other types, representing values that may be any one of those types.
So far, so good. However, when it comes to the properties of the united types
TypeScript will only allow an operation if it is valid for every member of the union.
If you think of your interfaces / types as sets of properties this would, mathematically speaking, be an intersection rather than a union. I have always considered this a misnomer. A fact that is also addressed by the documentation in small print:
It might be confusing that a union of types appears to have the intersection of those types’ properties. This is not an accident - the name union comes from type theory. The union
number | string
is composed by taking the union of the values from each type. Notice that given two sets with corresponding facts about each set, only the intersection of those facts applies to the union of the sets themselves. For example, if we had a room of tall people wearing hats, and another room of Spanish speakers wearing hats, after combining those rooms, the only thing we know about every person is that they must be wearing a hat.
Looking at the type definitions for Selection
and Transition
you will notice that there are no compatible methods in either type leaving you with an empty type featuring no properties at all. Hence, there is no way of calling a method .selectAll()
on that empty type which is basically what the error message is telling you.
There are two ways around that:
Use intersection types. You could modify your type definition to
Selection<BaseType, unknown, BaseType, unknown> & Transition<BaseType, unknown, BaseType, unknown>
Although this will work I would discourage the use because you are blending two distinct interfaces into one type to "just make it work". You are not extending an interface nor are you enriching it, but you are simply mixing in overloaded definitions of the .selectAll()
method which feels somewhat sketchy.
Selection
or a Transition
when using it. This can be done by employing an instanceof
type guard. That way, TypeScript is able to differentiate between both types:import { Selection, selection, Transition, transition, BaseType } from "d3";
function myFunc(
maybeTransition: Selection<BaseType, unknown, BaseType, unknown> |
Transition<BaseType, unknown, BaseType, unknown>
) {
let texts;
if (maybeTransition instanceof selection) { // type guard
texts = maybeTransition.selectAll("text"); // texts is of type Selection
} else if (maybeTransition instanceof transition) { // type guard
texts = maybeTransition.selectAll("text"); // texts is of type Transition
}
}
This might look clumsy but it is due to the type-safety of TypeScript itself. Although both Selection
as well as Transition
are modelled to look alike in Vanilla JavaScript the type-safety of TypeScript sometimes comes at the cost of more verbose code.
You also have to keep in mind that all of the above holds true for the texts
variable which is also a union of both types. You will have to deal with that the same way later on in your code. Depending on your overall design it might be worth considering a totally different approach, though.
Upvotes: 6
Reputation: 54998
The problem is the type of the 2 methods are incompatible, you need to discriminate them. A union could a solution :
function myFunc(param: { type: 'Selection', selection: Selection<BaseType, unknown, BaseType, unknown> } | { type: 'Transition', transition: Transition<BaseType, unknown, BaseType, unknown> }) {
if (param.type === 'Selection') {
const texts = param.selection.selectAll('text');
} else {
const texts = param.transition.selectAll('text');
}
// do something with texts
}
Upvotes: 1