'How to make data() key function to work with TypeScript?

I am trying to use a custom key function for .data() with .selectAll() selection in D3. But, making it TypeScript compatible is becoming such a pain.

The elements are not getting removed properly without the key function as D3 uses the index to check for updates by default.

So, here is the JavaScript code snippet, working all fine as expected:

const circles = svg
        .select("g")
        .selectAll("circle")
        .data(data, d => d.val);

Trying the same with TypeScript,

interface IData {
  val: number;
}

Attempt 1

const circles = svg
        .select("g")
        .selectAll("circle")
        .data<IData>(data, (d) => d.val);
Object d is of type 'unknown'.  TS2571

    35 |         .select("g")
    36 |         .selectAll("circle")
  > 37 |         .data<IData>(data, (d) => d.val);

Attempt 2

const circles = svg
        .select("g")
        .selectAll<SVGSVGElement, IData>("circle")
        .data<IData>(data, (d) => d.val);
No overload matches this call.
  Overload 1 of 3, '(data: IData[], key?: ValueFn<SVGSVGElement | Element | EnterElement | Document | Window | null, IData, string> | undefined): Selection<...>', gave the following error.
    Argument of type '(this: SVGSVGElement | Element | EnterElement | Document | Window | null, d: IData) => number' is not assignable to parameter of type 'ValueFn<SVGSVGElement | Element | EnterElement | Document | Window | null, IData, string>'.
      Type 'number' is not assignable to type 'string'.
  Overload 2 of 3, '(data: ValueFn<BaseType, unknown, IData[]>, key?: ValueFn<SVGSVGElement | Element | EnterElement | Document | Window | null, IData, string> | undefined): Selection<...>', gave the following error.
    Argument of type 'IData[]' is not assignable to parameter of type 'ValueFn<BaseType, unknown, IData[]>'.
      Type 'IData[]' provides no match for the signature '(this: BaseType, datum: unknown, index: number, groups: BaseType[] | ArrayLike<BaseType>): IData[]'.  TS2769

And, it is all working fine without the key function for data:

const circles = svg
        .select("g")
        .selectAll<SVGSVGElement, IData>("circle")
        .data<IData>(data);


Solution 1:[1]

As can be seen from the docs the key function needs to return a string (emphasis mine):

A key function may be specified to control which datum is assigned to which element, replacing the default join-by-index, by computing a string identifier for each datum and element.

This can also be seen by looking at the type definitions for selection.data():

data<NewDatum>(data: NewDatum[], key?: ValueFn<GElement | PElement, Datum | NewDatum, string>): Selection<GElement, NewDatum, PElement, PDatum>;

Herein, the key function is defined as

key?: ValueFn<GElement | PElement, Datum | NewDatum, string>

with the ValueFn type being

export type ValueFn<T extends BaseType, Datum, Result> = (this: T, datum: Datum, index: number, groups: T[] | ArrayLike<T>) => Result;

Looking at the above two definitions it is easy to see that the Result type parameter is set to string for the key function definition, which is the reason for your compiler error.

Returning a string from your key function will solve this issue:

.data(data, d => "" + d.val);

Solution 2:[2]

I’m using react + typescript generics. I finally figured this out, I think. I kept running into problems where using the old js pattern of data(d => d.val) confused the compiler as to what data type would pass afterward. Then I discovered the key function and struggled a bit with that as well. Finally, it seems to be happy, and the data type is consistent later on in the chain. Bear in mind, passing the generic key as props to my component helped, because I don’t need to get it from the d value.

…
.data(data, () => xKey as string)

I hope this helps someone!

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 altocumulus
Solution 2 Kraken