When implementing the example of a disjointed force-directed graph in โŒจ๏ธ TypeScript, d3.forceSimulation needs nodes of type SimulationNodeDatum. This requires implementing a specific type:

interface CustomNode extends d3.SimulationNodeDatum {
    id: string;
}
 
export function setupPlot(element: HTMLDivElement) {
	(...)
	 
    const nodes: CustomNode[] = [{
        id: "test-1"
    }, {
        id: "test-2"
    }]
 
    const simulation = d3.forceSimulation(nodes)
	(...)

for links any object with type { source: string; target: string; } is sufficient.

By default, nodes will be centred around (0, 0), corresponding to the view boxโ€™s top left corner. To show the nodes, the view box must be set to have (0, 0) at the centre using .attr("viewBox", [-width/2, -height/2, width, height]);

const svg = d3.create("svg")
	.attr("width", width)
	.attr("height", height)
	.attr("viewBox", [-width/2, -height/2, width, height]);

The rendered links and nodes must be updated on each simulation tick.

The final result

import * as d3 from "d3"
 
interface CustomNode extends d3.SimulationNodeDatum {
    id: string;
}
 
export function setupPlot(element: HTMLDivElement) {
    const width = 640;
    const height = 400;
 
    const nodes: CustomNode[] = [{
        id: "test-1"
    }, {
        id: "test-2"
    }]
    const links: d3.SimulationLinkDatum<CustomNode>[] = [{
        source: "test-1",
        target: "test-2"
    }]
 
    const simulation = d3.forceSimulation(nodes)
        .force("link", d3.forceLink(links).id(d=> (d as CustomNode).id))
        .force("charge", d3.forceManyBody())
        .force("x", d3.forceX())
        .force("y", d3.forceY());
 
    const svg = d3.create("svg")
        .attr("width", width)
        .attr("height", height)
        .attr("viewBox", [-width/2, -height/2, width, height]);
 
    // Add a line for each link, and a circle for each node.
    const link = svg.append("g")
        .attr("stroke", "#999")
        .attr("stroke-opacity", 0.6)
        .selectAll("line")
        .data(links)
        .join("line")
        .attr("stroke-width", 2);
 
    const node = svg.append("g")
        .attr("stroke", "#fff")
        .attr("stroke-width", 1.5)
        .selectAll("circle")
        .data(nodes)
        .join("circle")
        .attr("r", 5)
 
    simulation.on("tick", () => {
        link
            .attr("x1", d => (d.source as CustomNode).x!!)
            .attr("y1", d => (d.source as CustomNode).y!!)
            .attr("x2", d => (d.target as CustomNode).x!!)
            .attr("y2", d => (d.target as CustomNode).y!!);
 
        node
            .attr("cx", d => d.x!!)
            .attr("cy", d => d.y!!);
    })
 
    element.append(svg.node()!!);
}