Skip to content

Commit 29c72d5

Browse files
maxkfranzclaude
andcommitted
Narrow public node/edge types; drop docmaker allowlist (parity item #1)
Derive the public NodeCollection/EdgeCollection/NodeSingular/EdgeSingular by omitting the documented cross-kind members from the wide internal Collection/Element (EdgeOnlyKeys hidden from nodes, NodeOnlyKeys from edges). The runtime prototype stays shared, so Collection/Element remain wide internally; a new kind-agnostic SharedCollection base lets internal helpers accept any collection without casts, and the few mixed-collection algorithm spots are widened/cast locally. Upgrade the docmaker surface audit to resolve effective members through the TypeScript type checker (so it understands the Omit<>-based projections), which also lets the Core surface resolve fully. The audit now runs with no allowlisted cross-kind exceptions and api.test-d.ts asserts node/edge reject each other's methods. Closes the last of the three d.ts parity gaps (TYPESCRIPT-MIGRATION.md); the generated .d.ts is now the source-of-truth type surface. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2ec7907 commit 29c72d5

24 files changed

Lines changed: 310 additions & 253 deletions

TYPESCRIPT-MIGRATION.md

Lines changed: 44 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,21 @@ The runtime keeps its prototype-mixin structure; the types mirror it:
6060
via a shared permissive `Renderer` interface; params/locals/data are
6161
typed honestly.
6262

63-
## Parity with the old hand-written `index.d.ts` — remaining gap
64-
65-
The generated d.ts (~2,100 lines) covers the **full method surface** of
66-
`Core`, `Collection`, and `Element` — more method coverage than the
67-
hand-written file, since every mixin method is typed. The public entry
68-
point now also re-exports the old broad node/edge alias family
69-
(`Singular`, `NodeSingular`, `EdgeSingular`, `NodeCollection`,
70-
`EdgeCollection`), and the documented search/traversal helpers return
71-
those aliases where appropriate. The old `index.d.ts` (6,644 lines) is
72-
kept in the repo as a reference and parity target. A docmaker-based audit
73-
(`npm run test:types:docs`) now checks that the generated `build/dts`
74-
surface matches the documented `cy.*` / `eles.*` / `ele.*` API and
75-
guards the remaining known exceptions.
63+
## Parity with the old hand-written `index.d.ts`
64+
65+
The generated d.ts covers the **full method surface** of `Core`,
66+
`Collection`, and `Element` — more method coverage than the hand-written
67+
file, since every mixin method is typed. The public entry point also
68+
re-exports the old broad node/edge alias family (`Singular`,
69+
`NodeSingular`, `EdgeSingular`, `NodeCollection`, `EdgeCollection`), and the
70+
documented search/traversal helpers return those aliases where appropriate.
71+
The old `index.d.ts` (6,644 lines) is kept in the repo as a reference. A
72+
docmaker-based audit (`npm run test:types:docs`) checks that the generated
73+
`build/dts` surface matches the documented `cy.*` / `eles.*` / `ele.*` /
74+
`node.*` / `edge.*` API — with **no allowlisted exceptions**.
75+
76+
All three areas that previously diverged from the hand-written file are now
77+
reproduced from source:
7678

7779
**`Css.*` style-property types — done.** `src/style/css-types.mts` is now
7880
generated from the runtime style inventory (`src/style/properties.mts`) by
@@ -96,26 +98,24 @@ typed `any` on the base so the narrowed variants stay usable as handler
9698
parameters (function-parameter contravariance), matching the old declarations.
9799
`typescript/tests/api.test-d.ts` exercises all three target kinds.
98100

99-
One area of the hand-written file is **not yet reproduced** from source and is
100-
the remaining work to reach full parity:
101-
102-
1. **Node/edge public projections**`NodeCollection` and
103-
`EdgeCollection` are re-exported again, but they still structurally
104-
inherit some cross-kind methods from the wide internal `Collection`
105-
type in the generated declarations. Fully matching the docs requires a
106-
dedicated public node/edge projection layer that omits edge-only
107-
methods from node types and node-only methods from edge types. The exact
108-
cross-kind method lists currently tolerated live in the
109-
`allowedResidualExtras` allowlist in `test/types-docmaker-surface.mjs`
110-
(18 edge-only methods leaking onto `NodeCollection`, 58 node-only onto
111-
`EdgeCollection`).
112-
113-
Until then, downstream TypeScript users still see some node/edge
114-
cross-kind methods on the generated `NodeCollection` / `EdgeCollection`
115-
aliases.
116-
Reverting `package.json` `types` to `./index.d.ts` restores the richer
117-
hand-written types if that trade-off is preferred while the above source
118-
enrichment is completed.
101+
**Node/edge public projections — done.** `src/collection/eles-types.mts`
102+
keeps a wide internal `Collection`/`Element` (the shared prototype carries
103+
every element method at runtime, and internal code relies on that), and
104+
derives the public types by omitting the other kind's members:
105+
`NodeCollection`/`NodeSingular` drop the edge-only members (`source`,
106+
`target`, endpoints, edge geometry, …) and `EdgeCollection`/`EdgeSingular`
107+
drop the node-only members (compounds, degree, position, grab/lock,
108+
clustering, …). The omitted name lists (`EdgeOnlyKeys`/`NodeOnlyKeys`) are
109+
the documented cross-kind split, and a new kind-agnostic base
110+
`SharedCollection` lets internal helpers accept any collection without
111+
casts. The docmaker audit now resolves the effective member set through the
112+
TypeScript type checker (so it understands the `Omit<>`-based projections)
113+
and passes with no residual allowlist; `typescript/tests/api.test-d.ts`
114+
asserts (via `@ts-expect-error`) that node types reject edge-only methods
115+
and vice versa.
116+
117+
The generated `.d.ts` is now the source-of-truth type surface; the
118+
hand-written `index.d.ts` remains only as a historical reference.
119119

120120
## Current validation coverage and limits
121121

@@ -131,11 +131,13 @@ parity with the old hand-written `index.d.ts`:
131131
and representative usage, but it is not an exhaustive public API
132132
parity test.
133133
- `npm run test:types:docs` audits the freshly generated
134-
`build/dts/index.d.ts` against `documentation/docmaker.json`, but only
135-
for the broad documented surfaces represented as `cy.*`, `eles.*`, and
136-
`ele.*` plus the narrowed `NodeCollection` / `EdgeCollection` aliases.
137-
It is a doc-surface audit, not a symbol-for-symbol comparison against
138-
the old `index.d.ts`.
134+
`build/dts/index.d.ts` against `documentation/docmaker.json` for the
135+
documented `cy.*`, `eles.*`, `ele.*`, `node.*`, and `edge.*` surfaces. It
136+
resolves effective members through the TypeScript type checker (so it
137+
understands the `Omit<>`-based node/edge projections) and runs with **no
138+
residual allowlist** — it both rejects undocumented members and requires
139+
documented ones on each kind. It is a doc-surface audit, not a
140+
symbol-for-symbol comparison against the old `index.d.ts`.
139141
- `npm run test:types:css` audits the generated `Css.*` style surface
140142
against the live runtime property inventory in `src/style/properties.mts`,
141143
failing on any documented-but-untyped or typed-but-unknown property. It
@@ -145,17 +147,10 @@ parity with the old hand-written `index.d.ts`:
145147

146148
Known limitations of the current docmaker audit:
147149

148-
- It currently passes with an explicit allowlist of known residual
149-
exceptions for `NodeCollection` and `EdgeCollection` in
150-
`test/types-docmaker-surface.mjs`. Those exceptions are the remaining
151-
cross-kind methods inherited from the wide internal `Collection` type.
152-
- It enforces that undocumented `cy.*`, `eles.*`, and `ele.*` methods are
153-
not exposed on the generated declarations, but it does **not** yet give
154-
that same unconditional guarantee for narrowed node/edge projections.
155-
- It does not validate the exact overload shapes, generic constraints,
156-
or argument/return precision of every documented API entry; it checks
157-
presence/absence of the documented member names on the audited public
158-
interfaces.
150+
- It enforces presence/absence of documented member *names* on the audited
151+
public interfaces (`cy.*`, `eles.*`, `ele.*`, `node.*`, `edge.*`), but it
152+
does not validate the exact overload shapes, generic constraints, or
153+
argument/return precision of every documented API entry.
159154
- It does not assert full top-level export parity with the old manual
160155
declarations beyond the names used in the representative consumer test
161156
(e.g. broad alias-family presence like `NodeSingular` / `EdgeSingular`

dist/cytoscape.d.ts

Lines changed: 55 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -531,21 +531,21 @@ interface CollectionComparators {
531531
is(selector: string): boolean;
532532
some(fn: CollectionTestFn, thisArg?: unknown): boolean;
533533
every(fn: CollectionTestFn, thisArg?: unknown): boolean;
534-
same(collection: Collection | Element$1 | string): boolean;
535-
anySame(collection: Collection | Element$1 | string): boolean;
536-
allAreNeighbors(collection: Collection | Element$1 | string): boolean;
537-
allAreNeighbours(collection: Collection | Element$1 | string): boolean;
538-
contains(collection: Collection | Element$1 | string): boolean;
539-
has(collection: Collection | Element$1 | string): boolean;
534+
same(collection: SharedCollection | string): boolean;
535+
anySame(collection: SharedCollection | string): boolean;
536+
allAreNeighbors(collection: SharedCollection | string): boolean;
537+
allAreNeighbours(collection: SharedCollection | string): boolean;
538+
contains(collection: SharedCollection | string): boolean;
539+
has(collection: SharedCollection | string): boolean;
540540
}
541541
//#endregion
542542
//#region src/collection/filter.d.mts
543543
/** A predicate run against each element of a collection. */
544544
type FilterEleFn = (ele: Element$1, i: number, eles: Collection) => boolean | unknown;
545545
/** Inputs accepted by `.filter()` and friends: selector string, predicate, or collection. */
546-
type FilterArg = string | FilterEleFn | Collection | Element$1 | undefined | null;
546+
type FilterArg = string | FilterEleFn | SharedCollection | undefined | null;
547547
/** Inputs accepted by set operations: selector string or collection. */
548-
type SetArg = string | Collection | Element$1 | undefined | null;
548+
type SetArg = string | SharedCollection | undefined | null;
549549
/** Result of `.diff()`/`.difference()`. */
550550
interface DiffResult {
551551
left: Collection;
@@ -1500,12 +1500,12 @@ interface RemoveDataFunc<Self> {
15001500
//#region src/collection/data.d.mts
15011501
/** Data/scratch accessors contributed to the collection prototype. */
15021502
interface CollectionData {
1503-
data: DataFunc<Collection>;
1504-
removeData: RemoveDataFunc<Collection>;
1505-
scratch: DataFunc<Collection>;
1506-
removeScratch: RemoveDataFunc<Collection>;
1507-
attr: DataFunc<Collection>;
1508-
removeAttr: RemoveDataFunc<Collection>;
1503+
data: DataFunc<SharedCollection>;
1504+
removeData: RemoveDataFunc<SharedCollection>;
1505+
scratch: DataFunc<SharedCollection>;
1506+
removeScratch: RemoveDataFunc<SharedCollection>;
1507+
attr: DataFunc<SharedCollection>;
1508+
removeAttr: RemoveDataFunc<SharedCollection>;
15091509
id(): string | undefined;
15101510
}
15111511
//#endregion
@@ -1533,25 +1533,25 @@ type PositionFn = (ele: Element$1, i: number) => Position | undefined | false;
15331533
* full/partial Position, a (name, value) pair, or an event handler).
15341534
*/
15351535
interface PositionAccessor {
1536-
(this: Collection): Position;
1537-
(this: Collection, pos: Partial<Position>): Collection;
1538-
(this: Collection, name: string): number;
1539-
(this: Collection, name: string, value: number): Collection;
1540-
(this: Collection, handler: (...args: any[]) => void): Collection;
1536+
(this: SharedCollection): Position;
1537+
(this: SharedCollection, pos: Partial<Position>): Collection;
1538+
(this: SharedCollection, name: string): number;
1539+
(this: SharedCollection, name: string, value: number): Collection;
1540+
(this: SharedCollection, handler: (...args: any[]) => void): Collection;
15411541
}
15421542
/** Position accessor methods contributed to the collection prototype. */
15431543
interface CollectionPosition {
15441544
position: PositionAccessor;
15451545
modelPosition: PositionAccessor;
15461546
point: PositionAccessor;
1547-
positions(this: Collection, pos: Partial<Position> | PositionFn, silent?: boolean): Collection;
1548-
modelPositions(this: Collection, pos: Partial<Position> | PositionFn, silent?: boolean): Collection;
1549-
points(this: Collection, pos: Partial<Position> | PositionFn, silent?: boolean): Collection;
1550-
shift(this: Collection, dim: Partial<Position> | string, val?: number | boolean, silent?: boolean): Collection;
1551-
renderedPosition(this: Collection, dim?: Partial<Position> | string, val?: number): Position | number | Collection | undefined;
1552-
renderedPoint(this: Collection, dim?: Partial<Position> | string, val?: number): Position | number | Collection | undefined;
1553-
relativePosition(this: Collection, dim?: Partial<Position> | string, val?: number): Position | number | Collection | undefined;
1554-
relativePoint(this: Collection, dim?: Partial<Position> | string, val?: number): Position | number | Collection | undefined;
1547+
positions(this: SharedCollection, pos: Partial<Position> | PositionFn, silent?: boolean): Collection;
1548+
modelPositions(this: SharedCollection, pos: Partial<Position> | PositionFn, silent?: boolean): Collection;
1549+
points(this: SharedCollection, pos: Partial<Position> | PositionFn, silent?: boolean): Collection;
1550+
shift(this: SharedCollection, dim: Partial<Position> | string, val?: number | boolean, silent?: boolean): Collection;
1551+
renderedPosition(this: SharedCollection, dim?: Partial<Position> | string, val?: number): Position | number | Collection | undefined;
1552+
renderedPoint(this: SharedCollection, dim?: Partial<Position> | string, val?: number): Position | number | Collection | undefined;
1553+
relativePosition(this: SharedCollection, dim?: Partial<Position> | string, val?: number): Position | number | Collection | undefined;
1554+
relativePoint(this: SharedCollection, dim?: Partial<Position> | string, val?: number): Position | number | Collection | undefined;
15551555
}
15561556
//#endregion
15571557
//#region src/collection/dimensions/bounds.d.mts
@@ -1571,11 +1571,11 @@ interface BoundingBoxOptions {
15711571
}
15721572
/** Bounding-box methods contributed to the collection prototype. */
15731573
interface CollectionBounds {
1574-
renderedBoundingBox(this: Collection, options?: BoundingBoxOptions): BoundingBox;
1575-
renderedBoundingbox(this: Collection, options?: BoundingBoxOptions): BoundingBox;
1576-
boundingBox(this: Collection, options?: BoundingBoxOptions): BoundingBox;
1577-
boundingbox(this: Collection, options?: BoundingBoxOptions): BoundingBox;
1578-
bb(this: Collection, options?: BoundingBoxOptions): BoundingBox;
1574+
renderedBoundingBox(this: SharedCollection, options?: BoundingBoxOptions): BoundingBox;
1575+
renderedBoundingbox(this: SharedCollection, options?: BoundingBoxOptions): BoundingBox;
1576+
boundingBox(this: SharedCollection, options?: BoundingBoxOptions): BoundingBox;
1577+
boundingbox(this: SharedCollection, options?: BoundingBoxOptions): BoundingBox;
1578+
bb(this: SharedCollection, options?: BoundingBoxOptions): BoundingBox;
15791579
}
15801580
//#endregion
15811581
//#region src/collection/dimensions/width-height.d.mts
@@ -1725,11 +1725,11 @@ interface LayoutDimensions {
17251725
}
17261726
/** Layout methods contributed to the collection prototype. */
17271727
interface CollectionLayout {
1728-
layoutDimensions(this: Collection, options: LayoutDimensionsOptions): LayoutDimensions;
1729-
layoutPositions(this: Collection, layout: LayoutLike, options: LayoutOptions, fn: LayoutPositionFn): Collection;
1730-
layout(this: Collection, options?: Partial<LayoutOptions>): LayoutLike;
1731-
createLayout(this: Collection, options?: Partial<LayoutOptions>): LayoutLike;
1732-
makeLayout(this: Collection, options?: Partial<LayoutOptions>): LayoutLike;
1728+
layoutDimensions(this: SharedCollection, options: LayoutDimensionsOptions): LayoutDimensions;
1729+
layoutPositions(this: SharedCollection, layout: LayoutLike, options: LayoutOptions, fn: LayoutPositionFn): Collection;
1730+
layout(this: SharedCollection, options?: Partial<LayoutOptions>): LayoutLike;
1731+
createLayout(this: SharedCollection, options?: Partial<LayoutOptions>): LayoutLike;
1732+
makeLayout(this: SharedCollection, options?: Partial<LayoutOptions>): LayoutLike;
17331733
}
17341734
//#endregion
17351735
//#region src/collection/style.d.mts
@@ -1770,7 +1770,7 @@ interface CollectionSwitchFunctions {
17701770
//#endregion
17711771
//#region src/collection/traversing.d.mts
17721772
/** Selector/filter argument accepted by traversal methods. */
1773-
type SelectorArg = string | ((ele: Element$1, i: number, eles: Collection) => boolean | unknown) | Collection | Element$1 | undefined | null;
1773+
type SelectorArg = string | ((ele: Element$1, i: number, eles: Collection) => boolean | unknown) | SharedCollection | undefined | null;
17741774
interface CollectionTraversing {
17751775
roots(selector?: SelectorArg): NodeCollection;
17761776
leaves(selector?: SelectorArg): NodeCollection;
@@ -1921,9 +1921,14 @@ interface Collection extends CollectionBaseFns, CollectionAlgorithms, Collection
19211921
}
19221922
/** A single element (node or edge); array-like of itself, length 1. */
19231923
interface Element$1 extends Collection {}
1924-
type PublicSelectorArg = string | Collection | Element$1 | ((ele: Element$1, i: number, eles: Collection) => boolean | unknown) | undefined | null;
1924+
type PublicSelectorArg = string | SharedCollection | ((ele: Element$1, i: number, eles: Collection) => boolean | unknown) | undefined | null;
19251925
type Singular = Element$1;
1926-
interface NodeCollection extends Collection {
1926+
/** Edge-only members hidden from the public node types. */
1927+
type EdgeOnlyKeys = 'source' | 'target' | 'sources' | 'targets' | 'connectedNodes' | 'parallelEdges' | 'codirectedEdges' | 'isLoop' | 'isSimple' | 'midpoint' | 'controlPoints' | 'segmentPoints' | 'sourceEndpoint' | 'targetEndpoint' | 'renderedMidpoint' | 'renderedControlPoints' | 'renderedSegmentPoints' | 'renderedSourceEndpoint' | 'renderedTargetEndpoint';
1928+
/** Node-only members hidden from the public edge types. */
1929+
type NodeOnlyKeys = 'parent' | 'parents' | 'ancestors' | 'commonAncestors' | 'children' | 'siblings' | 'descendants' | 'roots' | 'leaves' | 'orphans' | 'nonorphans' | 'isParent' | 'isChild' | 'isChildless' | 'isOrphan' | 'connectedEdges' | 'edgesWith' | 'edgesTo' | 'degree' | 'indegree' | 'outdegree' | 'totalDegree' | 'minDegree' | 'maxDegree' | 'minIndegree' | 'maxIndegree' | 'minOutdegree' | 'maxOutdegree' | 'incomers' | 'outgoers' | 'successors' | 'predecessors' | 'position' | 'positions' | 'modelPosition' | 'modelPositions' | 'point' | 'points' | 'relativePosition' | 'relativePoint' | 'renderedPosition' | 'renderedPoint' | 'shift' | 'grabbable' | 'grabbed' | 'grabify' | 'ungrabify' | 'lock' | 'unlock' | 'locked' | 'layoutDimensions' | 'layoutPositions' | 'affinityPropagation' | 'ap' | 'fuzzyCMeans' | 'fcm' | 'hierarchicalClustering' | 'hca' | 'kMeans' | 'kMedoids';
1930+
/** Node-kind return-type narrowings layered over the wide collection. */
1931+
interface NodeCollectionNarrowed {
19271932
parent(selector?: PublicSelectorArg): NodeCollection;
19281933
parents(selector?: PublicSelectorArg): NodeCollection;
19291934
ancestors(selector?: PublicSelectorArg): NodeCollection;
@@ -1937,19 +1942,24 @@ interface NodeCollection extends Collection {
19371942
leaves(selector?: PublicSelectorArg): NodeCollection;
19381943
connectedEdges(selector?: PublicSelectorArg): EdgeCollection;
19391944
}
1940-
interface EdgeCollection extends Collection {
1945+
/** Edge-kind return-type narrowings layered over the wide collection. */
1946+
interface EdgeCollectionNarrowed {
19411947
source(selector?: PublicSelectorArg): NodeSingular;
19421948
target(selector?: PublicSelectorArg): NodeSingular;
19431949
sources(selector?: PublicSelectorArg): NodeCollection;
19441950
targets(selector?: PublicSelectorArg): NodeCollection;
1945-
edgesWith(otherNodes: string | Collection): EdgeCollection;
1946-
edgesTo(otherNodes: string | Collection): EdgeCollection;
19471951
connectedNodes(selector?: PublicSelectorArg): NodeCollection;
19481952
parallelEdges(selector?: PublicSelectorArg): EdgeCollection;
19491953
codirectedEdges(selector?: PublicSelectorArg): EdgeCollection;
19501954
}
1951-
type NodeSingular = Singular & NodeCollection;
1952-
type EdgeSingular = Singular & EdgeCollection;
1955+
type NodeNarrowedKeys = keyof NodeCollectionNarrowed;
1956+
type EdgeNarrowedKeys = keyof EdgeCollectionNarrowed;
1957+
/** Kind-agnostic collection: members common to both nodes and edges. */
1958+
type SharedCollection = Omit<Collection, EdgeOnlyKeys | NodeOnlyKeys>;
1959+
type NodeCollection = Omit<Collection, EdgeOnlyKeys | NodeNarrowedKeys> & NodeCollectionNarrowed;
1960+
type EdgeCollection = Omit<Collection, NodeOnlyKeys | EdgeNarrowedKeys> & EdgeCollectionNarrowed;
1961+
type NodeSingular = Omit<Element$1, EdgeOnlyKeys | NodeNarrowedKeys> & NodeCollectionNarrowed;
1962+
type EdgeSingular = Omit<Element$1, NodeOnlyKeys | EdgeNarrowedKeys> & EdgeCollectionNarrowed;
19531963
//#endregion
19541964
//#region src/style/css-types.d.mts
19551965
/**

src/collection/algorithms/betweenness-centrality.mts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Heap from '../../heap.mjs';
22
import * as util from '../../util/index.mjs';
3-
import type { Collection, Element } from '../eles-types.mjs';
3+
import type { Collection, SharedCollection, Element } from '../eles-types.mjs';
44

55
/** Edge weighting function. */
66
export type BetweennessWeightFn = ( edge: Element ) => number;
@@ -38,7 +38,7 @@ let elesfn = ({
3838

3939
// starting
4040
let V = this.nodes();
41-
let A: Record<string, Collection> = {};
41+
let A: Record<string, SharedCollection> = {};
4242
let _C: Record<string, number> = {};
4343
let max = 0;
4444
let C = {

0 commit comments

Comments
 (0)