@mnemonica/tactica
v0.1.0
Published
TypeScript Language Service Plugin for Mnemonica - generates types for nested constructors
Maintainers
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/tacticaUsage
1. As a TypeScript Language Service Plugin (Recommended)
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 tacticaWatch mode for development:
npx tactica --watchWith custom options:
npx tactica --project ./src/tsconfig.json --output ./types/mnemonica3. 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:
define()types work automatically: When you usedefine(), the returned constructor has the correct type signature with nested constructors.@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 AugmentedOrderGenerated 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
└── PhysicalProductTypeInstanceCode Coverage
Run tests with coverage:
npm run test:coverageIntegration 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
- Parse: TypeScript AST is parsed to find
define()anddecorate()calls - Analyze: The analyzer extracts type names, properties, and hierarchy
- Graph: A Trie (tree) structure represents the type hierarchy
- Generate: TypeScript declarations are generated from the graph
- 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
- Check that the plugin is loaded in
tsconfig.json - Restart TypeScript service (VS Code: Command Palette → "TypeScript: Restart TS Server")
- Verify file patterns in
include/excludeconfig
Generated types have errors
- Ensure all mnemonica types have explicit type annotations
- Check that
define()calls use string literals for type names - 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 errorRelated Projects
- mnemonica - Instance inheritance system
- @mnemonica/topologica - Filesystem-based type discovery
- typeomatica - Runtime type checking with
@Strictdecorator andBaseClass
Testing
Tactica includes comprehensive test coverage:
# Run all tests
npm test
# Run with coverage
npm run test:coverageTest 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.
