Files
n8n-mcp/src/types/node-types.ts
czlonkowski f3164e202f feat: add TypeScript type safety with strategic any assertions (v2.17.5)
Added comprehensive TypeScript type definitions for n8n node parsing while
maintaining zero compilation errors. Uses pragmatic "70% benefit with 0%
breakage" approach with strategic `any` assertions.

## Type Definitions (src/types/node-types.ts)
- NodeClass union type replaces `any` in method signatures
- Type guards: isVersionedNodeInstance(), isVersionedNodeClass()
- Utility functions for safe node handling

## Parser Updates
- node-parser.ts: All methods use NodeClass (15+ methods)
- simple-parser.ts: Strongly typed method signatures
- property-extractor.ts: Typed extraction methods
- 30+ method signatures improved

## Strategic Pattern
- Strong types in public method signatures (caller type safety)
- Strategic `as any` assertions for internal union type access
- Pattern: const desc = description as any; // Access union properties

## Benefits
- Better IDE support and auto-complete
- Compile-time safety at call sites
- Type-based documentation
- Zero compilation errors
- Bug prevention (would have caught v2.17.4 baseDescription issue)

## Test Updates
- All test files updated with `as any` for mock objects
- Zero compilation errors maintained

## Known Limitations
- ~70% type coverage (signatures typed, internal logic uses assertions)
- Union types (INodeTypeBaseDescription vs INodeTypeDescription) not fully resolved
- Future work: Conditional types or overloads for 100% type safety

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 22:16:59 +02:00

221 lines
6.2 KiB
TypeScript

/**
* TypeScript type definitions for n8n node parsing
*
* This file provides strong typing for node classes and instances,
* preventing bugs like the v2.17.4 baseDescription issue where
* TypeScript couldn't catch property name mistakes due to `any` types.
*
* @module types/node-types
* @since 2.17.5
*/
// Import n8n's official interfaces
import type {
IVersionedNodeType,
INodeType,
INodeTypeBaseDescription,
INodeTypeDescription
} from 'n8n-workflow';
/**
* Represents a node class that can be either:
* - A constructor function that returns INodeType
* - A constructor function that returns IVersionedNodeType
* - An already-instantiated node instance
*
* This covers all patterns we encounter when loading nodes from n8n packages.
*/
export type NodeClass =
| (new () => INodeType)
| (new () => IVersionedNodeType)
| INodeType
| IVersionedNodeType;
/**
* Instance of a versioned node type with all properties accessible.
*
* This represents nodes that use n8n's VersionedNodeType pattern,
* such as AI Agent, HTTP Request, Slack, etc.
*
* @property currentVersion - The computed current version (defaultVersion ?? max(nodeVersions))
* @property description - Base description stored as 'description' (NOT 'baseDescription')
* @property nodeVersions - Map of version numbers to INodeType implementations
*
* @example
* ```typescript
* const aiAgent = new AIAgentNode() as VersionedNodeInstance;
* console.log(aiAgent.currentVersion); // 2.2
* console.log(aiAgent.description.defaultVersion); // 2.2
* console.log(aiAgent.nodeVersions[1]); // INodeType for version 1
* ```
*/
export interface VersionedNodeInstance extends IVersionedNodeType {
currentVersion: number;
description: INodeTypeBaseDescription;
nodeVersions: {
[version: number]: INodeType;
};
}
/**
* Instance of a regular (non-versioned) node type.
*
* This represents simple nodes that don't use versioning,
* such as Edit Fields, Set, Code (v1), etc.
*/
export interface RegularNodeInstance extends INodeType {
description: INodeTypeDescription;
}
/**
* Union type for any node instance (versioned or regular).
*
* Use this when you need to handle both types of nodes.
*/
export type NodeInstance = VersionedNodeInstance | RegularNodeInstance;
/**
* Type guard to check if a node is a VersionedNodeType instance.
*
* This provides runtime type safety and enables TypeScript to narrow
* the type within conditional blocks.
*
* @param node - The node instance to check
* @returns True if node is a VersionedNodeInstance
*
* @example
* ```typescript
* const instance = new nodeClass();
* if (isVersionedNodeInstance(instance)) {
* // TypeScript knows instance is VersionedNodeInstance here
* console.log(instance.currentVersion);
* console.log(instance.nodeVersions);
* }
* ```
*/
export function isVersionedNodeInstance(node: any): node is VersionedNodeInstance {
return (
node !== null &&
typeof node === 'object' &&
'nodeVersions' in node &&
'currentVersion' in node &&
'description' in node &&
typeof node.currentVersion === 'number'
);
}
/**
* Type guard to check if a value is a VersionedNodeType class.
*
* This checks the constructor name pattern used by n8n's VersionedNodeType.
*
* @param nodeClass - The class or value to check
* @returns True if nodeClass is a VersionedNodeType constructor
*
* @example
* ```typescript
* if (isVersionedNodeClass(nodeClass)) {
* // It's a VersionedNodeType class
* const instance = new nodeClass() as VersionedNodeInstance;
* }
* ```
*/
export function isVersionedNodeClass(nodeClass: any): boolean {
return (
typeof nodeClass === 'function' &&
nodeClass.prototype?.constructor?.name === 'VersionedNodeType'
);
}
/**
* Safely instantiate a node class with proper error handling.
*
* Some nodes require specific parameters or environment setup to instantiate.
* This helper provides safe instantiation with fallback to null on error.
*
* @param nodeClass - The node class or instance to instantiate
* @returns The instantiated node or null if instantiation fails
*
* @example
* ```typescript
* const instance = instantiateNode(nodeClass);
* if (instance) {
* // Successfully instantiated
* const version = isVersionedNodeInstance(instance)
* ? instance.currentVersion
* : instance.description.version;
* }
* ```
*/
export function instantiateNode(nodeClass: NodeClass): NodeInstance | null {
try {
if (typeof nodeClass === 'function') {
return new nodeClass();
}
// Already an instance
return nodeClass;
} catch (e) {
// Some nodes require parameters to instantiate
return null;
}
}
/**
* Safely get a node instance, handling both classes and instances.
*
* This is a non-throwing version that returns undefined on failure.
*
* @param nodeClass - The node class or instance
* @returns The node instance or undefined
*/
export function getNodeInstance(nodeClass: NodeClass): NodeInstance | undefined {
const instance = instantiateNode(nodeClass);
return instance ?? undefined;
}
/**
* Extract description from a node class or instance.
*
* Handles both versioned and regular nodes, with fallback logic.
*
* @param nodeClass - The node class or instance
* @returns The node description or empty object on failure
*/
export function getNodeDescription(
nodeClass: NodeClass
): INodeTypeBaseDescription | INodeTypeDescription {
// Try to get description from instance first
try {
const instance = instantiateNode(nodeClass);
if (instance) {
// For VersionedNodeType, description is the baseDescription
if (isVersionedNodeInstance(instance)) {
return instance.description;
}
// For regular nodes, description is the full INodeTypeDescription
return instance.description;
}
} catch (e) {
// Ignore instantiation errors
}
// Fallback to static properties
if (typeof nodeClass === 'object' && 'description' in nodeClass) {
return nodeClass.description;
}
// Last resort: empty description
return {
displayName: '',
name: '',
group: [],
description: '',
version: 1,
defaults: { name: '', color: '' },
inputs: [],
outputs: [],
properties: []
} as any; // Type assertion needed for fallback case
}