-
Notifications
You must be signed in to change notification settings - Fork 879
Description
Current versus expected behaviour
TypeScript Module Resolution Issues with @joint/core
Problem Summary
When using @joint/core with modern TypeScript module resolution (moduleResolution: "bundler"`), TypeScript fails to properly resolve exports, requiring extensive module augmentation workarounds.
Issues Identified
1. Missing exports Field in package.json
The @joint/core package.json lacks an exports field, which is required for modern TypeScript module resolution strategies. This causes TypeScript to fail when resolving exports like shapes, V, g, etc.
Current package.json structure:
{
"main": "./dist/joint.min.js",
"module": "./dist/joint.js",
"types": "./types/index.d.ts"
}Expected (for modern module resolution):
{
"exports": {
".": {
"types": "./types/index.d.ts",
"import": "./dist/joint.js",
"require": "./dist/joint.min.js"
}
}
}2. Namespace/Value Merging Not Working
The g namespace is exported in the type definitions (types/index.d.ts), but when importing g as a value (import { g } from '@joint/core'), TypeScript doesn't recognize it as a namespace for type annotations.
Type definitions show:
// types/index.d.ts
export { g, ... } from './geometry';
// types/geometry.d.ts
export namespace g {
export class Point { ... }
export class Line { ... }
// ...
}Expected behavior:
import { g } from '@joint/core';
// Should work for both:
const point = new g.Point(1, 2); // ✅ Runtime value
function route(vertices: g.Point[]): g.Point[] { ... } // ✅ Type annotationActual behavior:
- Runtime usage works:
new g.Point()✅ - Type annotations fail:
g.Point[]❌ Error: "Cannot find namespace 'g'"
3. Nested Namespace/Class Exports Not Working (dia namespace)
The dia namespace contains both classes and nested namespaces (e.g., dia.Element is both a class and has a nested namespace with types like PortGroup, ConstructorOptions). When using modern module resolution, TypeScript fails to properly resolve these nested exports.
Type definitions show:
// types/joint.d.ts
export namespace dia {
export namespace Element {
interface PortGroup { ... }
interface ConstructorOptions extends Cell.ConstructorOptions { ... }
}
class Element extends Cell {
constructor(attributes?: DeepPartial<A>, opt?: Element.ConstructorOptions);
}
}Expected behavior:
import { dia } from '@joint/core';
// Should work for both:
const element = new dia.Element(...); // ✅ Runtime class
const options: dia.Element.ConstructorOptions = { ... }; // ✅ Type annotation
const portGroup: dia.Element.PortGroup = { ... }; // ✅ Nested namespace typeActual behavior:
- Runtime usage works:
new dia.Element()✅ - Type annotations fail:
dia.Element.ConstructorOptions❌ Error: "Namespace '"@joint/core".dia' has no exported member 'Element'" - Nested namespace types fail:
dia.Element.PortGroup❌ Same error
Workaround required:
We must use export import syntax in module augmentation to properly expose nested classes/namespaces:
export namespace dia {
export import Element = JointTypes.dia.Element; // Exposes both class and namespace
export import Link = JointTypes.dia.Link;
// ... many more
}However, this still doesn't fully resolve the issue - we also discovered that some type names are incorrect in documentation (e.g., dia.Element.Options doesn't exist, the correct type is dia.Element.ConstructorOptions).
4. Module Augmentation Limitations
To work around these issues, we need extensive module augmentation:
declare module '@joint/core' {
import type * as JointTypes from '@joint/core/types';
export const shapes: typeof JointTypes.shapes;
export const V: typeof JointTypes.V;
export namespace g {
export type Point = JointTypes.g.Point;
export type Line = JointTypes.g.Line;
// ... many more
}
export const g: typeof JointTypes.g;
export namespace dia {
export import Element = JointTypes.dia.Element; // Required for nested namespace access
export import Link = JointTypes.dia.Link;
// ... many more
}
export const dia: typeof JointTypes.dia;
// ... many more exports
}However, even with this augmentation:
- The
gnamespace/value merging still doesn't work properly, requiring additional local namespace declarations in files that usegfor type annotations - The
dianamespace requiresexport importsyntax to properly expose nested classes and their associated namespaces
Workaround Required
We currently need both:
- A centralized module augmentation file
- Local namespace declarations in files that use
gfor types:
// In each file that needs g.Point[] type annotations:
import { g } from '@joint/core';
import type * as GTypes from '@joint/core/types/geometry';
declare namespace g {
export type Point = GTypes.g.Point;
export type Line = GTypes.g.Line;
// ...
}Environment
- @joint/core version: 4.2.2
- TypeScript version: 5.8.3
- Module resolution:
bundler
Expected Solution
- Add proper
exportsfield to package.json for modern module resolution - Ensure namespace/value merging works correctly so
gcan be used as both a value and a namespace type - Ensure nested namespace/class exports work correctly for
dianamespace (e.g.,dia.Elementshould expose both the class and its nested namespace types likedia.Element.PortGroup,dia.Element.ConstructorOptions) - Verify that all exports (
shapes,V,g,dia, etc.) are properly exposed through the main entry point - Consider documenting correct type names (e.g.,
dia.Element.ConstructorOptionsnotdia.Element.Options)
Impact
This affects any TypeScript project using modern module resolution with @joint/core. The workarounds are:
- Error-prone (easy to forget local namespace declarations)
- Maintenance burden (duplicated type definitions)
- Not scalable (each file needs its own namespace declaration for
g) - Complex augmentation required for nested namespaces (
dia.Element,dia.Link, etc.) - Type name confusion (e.g.,
dia.Element.Optionsvsdia.Element.ConstructorOptions)
Related Files
node_modules/@joint/core/package.json- Missingexportsfieldnode_modules/@joint/core/types/index.d.ts- Types exist but not properly exposednode_modules/@joint/core/types/geometry.d.ts-gnamespace is defined herenode_modules/@joint/core/types/joint.d.ts-dianamespace and nested classes/namespaces defined here
Steps to reproduce
Setup
- Create a new TypeScript project with modern module resolution:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "bundler",
"strict": true
}
}- Install
@joint/core:
npm install @joint/coreIssue 1: Missing Exports - shapes and V Not Found
- Create a test file:
// test.ts
import { shapes, V } from '@joint/core';
const circle = new shapes.standard.Circle();- Run TypeScript compiler:
npx tsc --noEmitExpected: No errors
Actual:
error TS2305: Module '"@joint/core"' has no exported member 'shapes'.
error TS2305: Module '"@joint/core"' has no exported member 'V'.
Issue 2: g Namespace Not Available for Type Annotations
- Create a router file:
// router.ts
import { g } from '@joint/core';
function route(vertices: g.Point[]): g.Point[] {
const point = new g.Point(1, 2);
return [point];
}- Run TypeScript compiler:
npx tsc --noEmit router.tsExpected: No errors
Actual:
error TS2503: Cannot find namespace 'g'.
vertices: g.Point[]
~
error TS2503: Cannot find namespace 'g'.
function route(vertices: g.Point[]): g.Point[] {
~
Note: Runtime usage works (new g.Point()), but type annotations fail.
Issue 3: dia.Element Nested Namespace Not Available
- Create a custom cell file:
// customCell.ts
import { dia, shapes } from '@joint/core';
interface CellAttributes {
ports: {
groups: {
bottom: dia.Element.PortGroup;
};
};
}
class CustomCell extends shapes.standard.Circle {
constructor(attributes?: Partial<CellAttributes>, options?: dia.Element.ConstructorOptions) {
super(attributes, options);
}
}- Run TypeScript compiler:
npx tsc --noEmit customCell.tsExpected: No errors
Actual:
error TS2694: Namespace '"@joint/core".dia' has no exported member 'Element'.
groups: {
bottom: dia.Element.PortGroup;
~~~~~~
error TS2694: Namespace '"@joint/core".dia' has no exported member 'Element'.
options?: dia.Element.ConstructorOptions
~~~~~~
Issue 4: Module Augmentation Doesn't Fully Resolve Issues
- Create a module augmentation file:
// jointjs-augmentation.ts
declare module '@joint/core' {
import type * as JointTypes from '@joint/core/types';
export const shapes: typeof JointTypes.shapes;
export const V: typeof JointTypes.V;
export namespace g {
export type Point = JointTypes.g.Point;
export type Line = JointTypes.g.Line;
}
export const g: typeof JointTypes.g;
export namespace dia {
export import Element = JointTypes.dia.Element;
}
export const dia: typeof JointTypes.dia;
}- Import the augmentation in your router file:
// router.ts
import { g } from '@joint/core';
import './jointjs-augmentation';
// Import augmentation
function route(vertices: g.Point[]): g.Point[] {
// Still fails!
return [];
}- Run TypeScript compiler:
npx tsc --noEmit router.tsExpected: No errors after augmentation
Actual: Still fails with Cannot find namespace 'g' error
Workaround Required: Add local namespace declaration in each file:
// router.ts
import { g } from '@joint/core';
import type * as GTypes from '@joint/core/types/geometry';
import './jointjs-augmentation';
// Local workaround needed
declare namespace g {
export type Point = GTypes.g.Point;
export type Line = GTypes.g.Line;
}
function route(vertices: g.Point[]): g.Point[] {
// Now works
return [];
}Version
4.2.2
What browsers are you seeing the problem on?
No response
What operating system are you seeing the problem on?
Windows