Skip to main content

Overview

The TypeInfo generation system converts Microsoft API Extractor’s API model into a structured JSON format compatible with Mintlify’s <TypeTree> component. The key challenge is fully recursive type expansion - extracting not just top-level properties, but all nested object structures with their complete documentation.

Architecture

Core Components

The TypeInfo generation pipeline consists of:
ApiModel → TypeInfoGenerator → TypeInfo.jsx + TypeInfo.d.ts
   ↓              ↓                        ↓
*.api.json   Recursive        JavaScript + TypeScript
(API Extractor) Resolution      (with autocomplete)

TypeInfoGenerator (src/utils/TypeInfoGenerator.ts)

The main orchestrator responsible for:
  • Loading API models from API Extractor output
  • Recursively processing API items (interfaces, classes, type aliases)
  • Resolving type references across the API model
  • Converting to TypeTreeProperty-compatible format
  • Generating both .jsx and .d.ts files

ObjectTypeAnalyzer (src/utils/ObjectTypeAnalyzer.ts)

Utility for parsing complex TypeScript type strings:
  • Parses inline object type literals
  • Extracts nested properties, unions, intersections
  • Handles generics and array types
  • Returns structured TypeAnalysis objects

CacheManager (src/cache/)

Performance optimization layer:
  • Caches parsed type strings (TypeAnalysisCache)
  • Caches API item lookups (ApiResolutionCache)
  • LRU eviction for memory management

The Recursive Type Resolution Algorithm

Problem Statement

Given a TypeScript interface like:
interface ResolvedConfig {
  templates: ResolvedTemplateConfig;
}

interface ResolvedTemplateConfig {
  cache: boolean;
  strict: boolean;
  userTemplateDir?: string;
}
We need to generate TypeInfo with fully nested documentation:
{
  name: "templates",
  type: "object",
  description: "Resolved template configuration...",
  properties: [
    {
      name: "cache",
      type: "boolean",
      description: "Whether template caching is enabled",
      required: true
    },
    // ... more properties with descriptions
  ]
}

Core Algorithm

The resolution happens in _extractNestedProperties():
private _extractNestedProperties(typeString: string): TypeInfo[] | null {
  const trimmedType = typeString.trim();

  // Step 1: Check if it's a type reference (not inline object)
  if (!trimmedType.includes('{')) {
    // Try to find the referenced interface/type in the API model
    const referencedItem = this._findApiItemByName(trimmedType);

    if (referencedItem && referencedItem.kind === ApiItemKind.Interface) {
      const interfaceItem = referencedItem as ApiInterface;
      const properties: TypeInfo[] = [];

      // Step 2: Recursively process each member
      for (const member of interfaceItem.members) {
        if (member.kind === ApiItemKind.PropertySignature) {
          // THIS IS THE KEY: _processProperty calls _extractNestedProperties
          // Creating a recursive loop that follows type references
          const propInfo = this._processProperty(member as ApiPropertyItem);
          if (propInfo) {
            properties.push(propInfo);
          }
        }
      }

      return properties.length > 0 ? properties : null;
    }
  }

  // Step 3: Fall back to parsing inline object types
  if (trimmedType.includes('{') && trimmedType.includes('}')) {
    const analysis = this._typeAnalyzer.analyzeType(typeString);

    if (analysis.type === 'object-literal' && analysis.properties) {
      return this._convertPropertiesToTypeInfo(analysis.properties);
    }
  }

  return null;
}

Why Named Interfaces Are Critical

API Extractor Limitation: JSDoc comments are not preserved for properties within inline object type literals. Only named interfaces preserve full documentation.
Bad (loses documentation):
interface Config {
  // This inline object won't have property descriptions
  templates: {
    cache: boolean;
    strict: boolean;
  }
}
Good (preserves documentation):
interface Config {
  templates: ResolvedTemplateConfig; // Reference to named interface
}

interface ResolvedTemplateConfig {
  /** Whether template caching is enabled */
  cache: boolean;
  /** Whether strict mode is enabled */
  strict: boolean;
}
The algorithm handles both cases:
  1. Named references: Looks up in API model → gets full docs recursively
  2. Inline objects: Parses structure only → no property-level docs

Key Methods

_processProperty()

Converts an API Extractor property item to TypeInfo format:
private _processProperty(apiProperty: ApiPropertyItem): TypeInfo | null {
  const typeString = apiProperty.propertyTypeExcerpt.text;

  // Extract metadata
  const isRequired = !apiProperty.isOptional;
  const isReadonly = apiProperty.isReadonly;
  const isDeprecated = apiProperty.tsdocComment?.deprecatedBlock !== undefined;
  const defaultValue = this._extractDefaultValue(apiProperty);

  // THIS IS WHERE RECURSION HAPPENS
  const nestedProperties = this._extractNestedProperties(typeString);

  return {
    name: apiProperty.displayName,
    type: nestedProperties ? 'object' : this._simplifyType(typeString),
    description: this._getDescription(apiProperty),
    required: isRequired,
    deprecated: isDeprecated || undefined,
    defaultValue: defaultValue || undefined,
    properties: nestedProperties || undefined
  };
}

_findApiItemByName()

Critical for type reference resolution:
private _findApiItemByName(name: string): ApiItem | undefined {
  for (const packageItem of this._apiModel.packages) {
    if (packageItem.entryPoints.length > 0) {
      const entryPoint = packageItem.entryPoints[0];
      for (const member of entryPoint.members) {
        if (member.displayName === name) {
          return member;
        }
      }
    }
  }
  return undefined;
}
Limitation: Currently only searches entry point members. Doesn’t handle:
  • Nested namespace members
  • Re-exported types from external packages
  • Types in different entry points

_convertTypeAnalysisToString()

Converts parsed type structures back to readable type strings:
private _convertTypeAnalysisToString(analysis: TypeAnalysis): string {
  switch (analysis.type) {
    case 'primitive':
      return analysis.name || 'unknown';

    case 'array':
      return `${this._convertTypeAnalysisToString(analysis.elementType)}[]`;

    case 'union':
      return analysis.unionTypes
        .map(t => this._convertTypeAnalysisToString(t))
        .join(' | ');

    case 'intersection':
      return analysis.intersectionTypes
        .map(t => this._convertTypeAnalysisToString(t))
        .join(' & ');

    case 'generic':
      return `${analysis.baseType}<${analysis.typeParameters.join(', ')}>`;

    case 'object-literal':
      return 'object'; // Nested properties in separate field

    default:
      return 'unknown';
  }
}

Output Generation

TypeInfo.jsx

JavaScript module with the complete type structure:
/**
 * Type information organized by package and API item.
 * Use this to get TypeTreeProperty-compatible data for any documented type.
 *
 * @example
 * ```jsx
 * import { TypeInfo } from "/snippets/tsdocs/TypeInfo.jsx"
 * <TypeTree {...TypeInfo.MyPackage.MyInterface} />
 * ```
 */
export const TypeInfo = {
  "PackageName": {
    "TypeName": {
      name: "TypeName",
      type: "interface",
      description: "...",
      properties: [...]
    }
  }
};

TypeInfo.d.ts

TypeScript declaration for IDE autocomplete:
/**
 * TypeTree property structure
 */
export interface TypeTreeProperty {
  name: string;
  type: string;
  description?: string;
  required?: boolean;
  deprecated?: boolean;
  defaultValue?: string;
  properties?: TypeTreeProperty[];
}

/**
 * Type information organized by package and API item.
 */
export const TypeInfo: {
  "PackageName": {
    "TypeName": TypeTreeProperty;
  }
};

Performance Considerations

Caching Strategy

The system uses two-level caching:
  1. Type Analysis Cache (ObjectTypeAnalyzer)
    • Caches parsed type strings
    • Key: raw type string
    • Value: TypeAnalysis object
    • Prevents redundant parsing of common types
  2. API Resolution Cache (Future optimization)
    • Could cache _findApiItemByName() lookups
    • Currently uses JSON.stringify() for keys (slow)
    • Opportunity for improvement with better key generation

Recursion Depth

The recursive algorithm naturally terminates because:
  1. TypeScript doesn’t allow circular type references at the value level
  2. Each recursion processes a unique API item
  3. Primitive types (string, number, etc.) end the recursion
No explicit depth limit is needed, though one could be added for safety.

Integration Points

MarkdownDocumenter

The TypeInfoGenerator is called from MarkdownDocumenter._writeApiItemPage():
// Generate TypeInfo files
const typeInfoGenerator = new TypeInfoGenerator(this._apiModel);
const typeInfoContent = typeInfoGenerator.generateTypeInfoModule();
const typeInfoDeclaration = typeInfoGenerator.generateTypeInfoDeclaration();

await FileSystem.writeFile(
  path.join(snippetsDir, 'TypeInfo.jsx'),
  typeInfoContent
);

await FileSystem.writeFile(
  path.join(snippetsDir, 'TypeInfo.d.ts'),
  typeInfoDeclaration
);

Configuration

TypeInfo generation is always enabled and runs automatically during mint-tsdocs generate. There are currently no configuration options to disable or customize it.

Known Limitations

1. Entry Point Scope

_findApiItemByName() only searches the first entry point of each package. This means: Won’t resolve:
  • Types in nested namespaces
  • Re-exported types from external packages
  • Types in additional entry points
Will resolve:
  • Top-level interfaces, classes, type aliases
  • Types in the same package as the referencing property

2. Inline Object Types

Properties within inline object type literals don’t have descriptions:
// Won't have property descriptions in TypeInfo
interface Config {
  options: { foo: string; bar: number; }
}
Solution: Always use named interfaces for types that need documentation.

3. Complex Type References

Advanced TypeScript features may not be fully resolved:
  • Conditional types
  • Mapped types
  • Template literal types
  • Utility types (Partial, Pick, etc.)
These will show the raw type string without expansion.

Future Improvements

1. Enhanced Type Resolution

  • Support for nested namespace members
  • Cross-package type resolution
  • Handling of re-exported types

2. Smarter Caching

  • Replace JSON.stringify() in API resolution cache
  • Implement TTL for cache entries
  • Statistics for cache hit rates

3. Configuration Options

  • Option to disable TypeInfo generation
  • Control over recursion depth
  • Custom type transformers

4. Error Handling

  • Better diagnostics when type resolution fails
  • Warnings for unresolved type references
  • Validation of generated TypeInfo structure

Testing

TypeInfo generation is tested through:
  1. Snapshot tests: Verify output structure remains consistent
  2. Integration tests: Run full generation pipeline on test projects
  3. Manual verification: Check generated files in actual documentation
To test TypeInfo generation:
# Run full test suite
bun test

# Update snapshots after intentional changes
bun test -- -u

# Test on actual project
mint-tsdocs generate
cat docs/snippets/tsdocs/TypeInfo.jsx

References