JSPM

@mnemonica/tactica

0.1.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 1
  • Score
    100M100P100Q23693F
  • License MIT

TypeScript Language Service Plugin for Mnemonica - generates types for nested constructors

Package Exports

  • @mnemonica/tactica
  • @mnemonica/tactica/lib/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@mnemonica/tactica) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

@mnemonica/tactica

TypeScript Language Service Plugin for Mnemonica

Tactica generates type definitions for Mnemonica's dynamic nested constructors, enabling TypeScript to understand runtime type hierarchies created through define() and decorate() calls.

The Problem

Mnemonica enables powerful instance-level inheritance:

const UserType = define('UserType', function (this: { name: string }) {
    this.name = '';
});

const AdminType = UserType.define('AdminType', function (this: { role: string }) {
    this.role = 'admin';
});

const user = new UserType();
const admin = new user.AdminType(); // Works at runtime!

But TypeScript doesn't know that user.AdminType exists because UserType.define() is a runtime operation.

The Solution

Tactica analyzes your TypeScript source files and generates declaration files that tell TypeScript about the nested constructor hierarchy.

Installation

npm install --save-dev @mnemonica/tactica

Usage

Add to your tsconfig.json:

{
  "compilerOptions": {
    "plugins": [
      {
        "name": "@mnemonica/tactica",
        "outputDir": ".tactica",
        "include": ["src/**/*.ts"],
        "exclude": ["**/*.test.ts", "**/*.spec.ts"]
      }
    ]
  }
}

Then include the generated types in your project:

{
  "compilerOptions": {
    "typeRoots": ["./node_modules/@types", "./.tactica"]
  }
}

2. As a CLI Tool

Generate types once:

npx tactica

Watch mode for development:

npx tactica --watch

With custom options:

npx tactica --project ./src/tsconfig.json --output ./types/mnemonica

3. As a Module

import { MnemonicaAnalyzer, TypesGenerator, TypesWriter } from '@mnemonica/tactica';
import * as ts from 'typescript';

const program = ts.createProgram(['./src/index.ts'], {});
const analyzer = new MnemonicaAnalyzer(program);

for (const sourceFile of program.getSourceFiles()) {
    if (!sourceFile.isDeclarationFile) {
        analyzer.analyzeFile(sourceFile);
    }
}

const generator = new TypesGenerator(analyzer.getGraph());

// Generate types.ts (exportable type aliases - default mode)
const generatedTypes = generator.generateTypesFile();
const writer = new TypesWriter('.tactica');
writer.writeTypesFile(generatedTypes);

// Or generate index.d.ts (global augmentation - legacy mode)
const generatedGlobal = generator.generateGlobalAugmentation();
writer.writeGlobalAugmentation(generatedGlobal);

Configuration Options

Plugin Options (tsconfig.json)

Option Type Default Description
outputDir string .tactica Directory for generated types
include string[] ['**/*.ts'] File patterns to include
exclude string[] ['**/*.d.ts'] File patterns to exclude
verbose boolean false Enable verbose logging

CLI Options

Option Short Description
--watch -w Watch mode - regenerate on file changes
--project -p Path to tsconfig.json
--output -o Output directory (default: .tactica)
--include -i Include patterns (comma-separated)
--exclude -e Exclude patterns (comma-separated)
--module-augmentation -m Generate global augmentation (legacy mode)
--verbose -v Enable verbose logging
--help -h Show help message

Examples:

# Default mode - generates .tactica/types.ts
npx tactica

# Global augmentation mode - generates .tactica/index.d.ts
npx tactica --module-augmentation

# Watch mode with custom output directory
npx tactica --watch --output ./custom-types

# Exclude test files
npx tactica --exclude "*.test.ts,*.spec.ts"

Generated Output

By default, Tactica generates .tactica/types.ts with exported type aliases:

// Generated by @mnemonica/tactica - DO NOT EDIT
export type UserTypeInstance = {
    name: string;
    email: string;
    AdminType: TypeConstructor<AdminTypeInstance>;
}

export type AdminTypeInstance = UserTypeInstance & {
    role: string;
    permissions: string[];
}

Output Modes

Default mode (npx tactica):

  • Generates .tactica/types.ts - Exportable type aliases
  • Import types explicitly: import type { UserTypeInstance } from './.tactica/types'
  • Include in tsconfig.json: "include": ["src/**/*.ts", ".tactica/types.ts"]
  • Recommended for new projects - explicit imports, better tree-shaking

Global mode (npx tactica --module-augmentation):

  • Generates .tactica/index.d.ts - Global type declarations
  • Types are available without imports (via declare global)
  • Add to tsconfig.json: "typeRoots": ["./node_modules/@types", "./.tactica"]
  • Use triple-slash reference: /// <reference types="./.tactica/index" />

Choosing a mode:

  • Use Default mode for new projects - explicit imports are clearer and work better with tree-shaking
  • Use Global mode if you want types available without imports (legacy behavior)

What Gets Analyzed

1. define() Calls

// Root type
const UserType = define('UserType', function (this: { name: string }) {
    this.name = '';
});

// Nested type
const AdminType = UserType.define('AdminType', function (this: { role: string }) {
    this.role = 'admin';
});

2. @decorate() Decorator

@decorate()
class User {
    name: string = '';
}

@decorate(User)
class Admin {
    role: string = 'admin';
}

Why Type Casting is Necessary for @decorate()

When using @decorate() on classes, TypeScript cannot automatically infer that instances have nested type constructors (like user.Admin). This is because:

  1. define() types work automatically: When you use define(), the returned constructor has the correct type signature with nested constructors.

  2. @decorate() classes need casting: When you decorate a class, TypeScript sees the class itself, not the augmented type that mnemonica creates at runtime.

The Solution: Cast to the instance type to access nested constructors:

@decorate()
class Order {
    orderId: string = '';
    total: number = 0;
}

@decorate(Order)
class AugmentedOrder {
    addition: string = 'extra';
}

// Cast to OrderInstance to access AugmentedOrder constructor
const order = new Order() as OrderInstance;
const augmented = new order.AugmentedOrder(); // ✅ Works!

// The 'augmented' variable is automatically typed as AugmentedOrderInstance
console.log(augmented.orderId);  // From Order
console.log(augmented.addition); // From AugmentedOrder

Generated types (like OrderInstance, AugmentedOrderInstance) are automatically available globally - no imports needed!

@decorate() with Options

@decorate({
    blockErrors: true,
    strictChain: false,
    exposeInstanceMethods: true
})
class ConfigurableClass {
    value: string = '';
}

3. Object.assign Pattern

const UserType = define('UserType', function (this: any, data: any) {
    Object.assign(this, data);
});

4. Typeomatica Integration

Tactica works seamlessly with Typeomatica patterns:

import { decorate } from 'mnemonica';
import { Strict, BaseClass } from 'typeomatica';

// @Strict decorator alongside @decorate
@decorate()
@Strict({ someProp: 123 })
class StrictDecorated {
    someProp!: number;
}

// BaseClass with Object.setPrototypeOf
@decorate()
class MyBaseClass {
    base_field = 555;
}

Object.setPrototypeOf(MyBaseClass.prototype, new BaseClass({ strict: true }));

5. ConstructorFunction Pattern

import { define, ConstructorFunction } from 'mnemonica';

const MyFn = function (this: any) {
    this.field = 123;
} as ConstructorFunction<{ field: number }>;

const MyFnType = define('MyFnType', MyFn);

CLI Features

Tree Output

The CLI displays type hierarchy as a tree:

$ npx tactica

Type Hierarchy (Trie):
└── UserTypeInstance
    └── AdminTypeInstance
        └── SuperAdminTypeInstance
└── ProductTypeInstance
    ├── DigitalProductTypeInstance
    └── PhysicalProductTypeInstance

Code Coverage

Run tests with coverage:

npm run test:coverage

Integration with Your Workflow

.gitignore

Tactica automatically adds .tactica/ to your .gitignore if not already present.

IDE Support

With the Language Service Plugin:

  • VS Code: Automatic type updates on file save
  • WebStorm: Works with TypeScript service
  • Vim/Neovim: Works with coc.nvim, nvim-lspconfig

API Reference

MnemonicaAnalyzer

class MnemonicaAnalyzer {
    constructor(program?: ts.Program);
    analyzeFile(sourceFile: ts.SourceFile): AnalyzeResult;
    analyzeSource(sourceCode: string, fileName?: string): AnalyzeResult;
    getGraph(): TypeGraphImpl;
}

TypeGraphImpl

class TypeGraphImpl implements TypeGraph {
    roots: Map<string, TypeNode>;
    allTypes: Map<string, TypeNode>;
    addRoot(node: TypeNode): void;
    addChild(parent: TypeNode, child: TypeNode): void;
    findType(fullPath: string): TypeNode | undefined;
    getAllTypes(): TypeNode[];
    *bfs(): Generator<TypeNode>;
    *dfs(node?: TypeNode): Generator<TypeNode>;
}

TypesGenerator

class TypesGenerator {
    constructor(graph: TypeGraphImpl);
    generate(): GeneratedTypes;
    generateSingleType(node: TypeNode): string;
}

TypesWriter

class TypesWriter {
    constructor(outputDir?: string);
    write(generated: GeneratedTypes): string;
    writeTo(filename: string, content: string): string;
    clean(): void;
    getOutputDir(): string;
}

How It Works

  1. Parse: TypeScript AST is parsed to find define() and decorate() calls
  2. Analyze: The analyzer extracts type names, properties, and hierarchy
  3. Graph: A Trie (tree) structure represents the type hierarchy
  4. Generate: TypeScript declarations are generated from the graph
  5. Output: Files are written to .tactica/ directory
Type Hierarchy (Trie)
├── UserType
│   ├── properties: { name: string }
│   └── AdminType
│       ├── properties: { role: string }
│       └── SuperAdminType
│           └── properties: { permissions: string[] }
└── OrderType
    └── properties: { items: Item[] }

Troubleshooting

Types not updating

  1. Check that the plugin is loaded in tsconfig.json
  2. Restart TypeScript service (VS Code: Command Palette → "TypeScript: Restart TS Server")
  3. Verify file patterns in include/exclude config

Generated types have errors

  1. Ensure all mnemonica types have explicit type annotations
  2. Check that define() calls use string literals for type names
  3. Verify property types are valid TypeScript

Plugin not working

# Test with CLI first
npx tactica --verbose

# Check for parsing errors
npx tactica --verbose 2>&1 | grep -i error

Testing

Tactica includes comprehensive test coverage:

# Run all tests
npm test

# Run with coverage
npm run test:coverage

Test suites include:

  • Analyzer tests - Core AST parsing functionality
  • Generator tests - TypeScript declaration generation
  • Writer tests - File I/O operations
  • Integration tests - End-to-end workflows
  • Example tests - Patterns from tactica-test/ project
  • Typeomatica tests - Combined mnemonica + typeomatica patterns

License

MIT

Contributing

Contributions welcome! Please read the Contributing Guide for details.