Skip to content

[Bug]: TypeScript Module Resolution Issues with @joint/core #3140

@psaju

Description

@psaju

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 annotation

Actual 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 type

Actual 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 g namespace/value merging still doesn't work properly, requiring additional local namespace declarations in files that use g for type annotations
  • The dia namespace requires export import syntax to properly expose nested classes and their associated namespaces

Workaround Required

We currently need both:

  1. A centralized module augmentation file
  2. Local namespace declarations in files that use g for 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

  1. Add proper exports field to package.json for modern module resolution
  2. Ensure namespace/value merging works correctly so g can be used as both a value and a namespace type
  3. Ensure nested namespace/class exports work correctly for dia namespace (e.g., dia.Element should expose both the class and its nested namespace types like dia.Element.PortGroup, dia.Element.ConstructorOptions)
  4. Verify that all exports (shapes, V, g, dia, etc.) are properly exposed through the main entry point
  5. Consider documenting correct type names (e.g., dia.Element.ConstructorOptions not dia.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.Options vs dia.Element.ConstructorOptions)

Related Files

  • node_modules/@joint/core/package.json - Missing exports field
  • node_modules/@joint/core/types/index.d.ts - Types exist but not properly exposed
  • node_modules/@joint/core/types/geometry.d.ts - g namespace is defined here
  • node_modules/@joint/core/types/joint.d.ts - dia namespace and nested classes/namespaces defined here

Steps to reproduce

Setup

  1. Create a new TypeScript project with modern module resolution:
// tsconfig.json
{
    "compilerOptions": {
        "target": "ES2020",
        "module": "NodeNext",
        "moduleResolution": "bundler",
        "strict": true
    }
}
  1. Install @joint/core:
npm install @joint/core

Issue 1: Missing Exports - shapes and V Not Found

  1. Create a test file:
// test.ts
import { shapes, V } from '@joint/core';

const circle = new shapes.standard.Circle();
  1. Run TypeScript compiler:
npx tsc --noEmit

Expected: 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

  1. 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];
}
  1. Run TypeScript compiler:
npx tsc --noEmit router.ts

Expected: 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

  1. 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);
    }
}
  1. Run TypeScript compiler:
npx tsc --noEmit customCell.ts

Expected: 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

  1. 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;
}
  1. 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 [];
}
  1. Run TypeScript compiler:
npx tsc --noEmit router.ts

Expected: 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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions