/**
* Per-declaration extractors for TypeScript type aliases, interfaces, and enums.
*
* `extractTypeInfo` handles type aliases and interfaces (including index/method/
* call/construct signatures and intersection filtering). `extractEnumInfo`
* handles regular and const enums. Both mutate a `DeclarationJsonBuild` with
* rich metadata derived from the TypeScript checker. Called by
* `analyzeDeclaration` in `typescript-exports.ts` once kind dispatch is
* settled.
*
* Type-alias property walking (named properties, index/call/construct
* signatures) lives in `typescript-extract-type-properties.ts` to keep this
* module focused on top-level dispatch.
*
* @see `typescript-extract-shared.ts` for shared helpers
* @see `typescript-extract-type-properties.ts` for type-alias property walking
*
* @module
*/
import ts from 'typescript';
import type {DeclarationJsonBuild, MemberJsonBuild} from './declaration-build.js';
import {type Diagnostic} from './diagnostics.js';
import {parseComment, applyToDeclaration} from './tsdoc.js';
import {type IsExternalFile} from './typescript-program.js';
import {
emitCallOrConstructSignature,
extractModifiers,
getNodeLocation,
parseGenericParam,
populateCallableMember,
} from './typescript-extract-shared.js';
import {extractTypeAliasProperties} from './typescript-extract-type-properties.js';
/**
* Extract type/interface information with rich property metadata.
*
* @internal Used by `analyzeDeclaration` — not part of the public barrel export.
*
* @param node - the declaration AST node
* @param checker - TypeScript type checker
* @param declaration - the declaration to populate
* @param diagnostics - diagnostics collector for non-fatal issues
* @mutates declaration - adds typeSignature, genericParams, extends, intersects, members (and `partial: true` on extraction failure)
* @mutates diagnostics - adds `type_extraction_failed` / `signature_analysis_failed` diagnostics on checker errors
*/
export const extractTypeInfo = (
node: ts.Node,
checker: ts.TypeChecker,
declaration: DeclarationJsonBuild,
diagnostics: Array<Diagnostic>,
isExternalFile: IsExternalFile,
): void => {
let nodeType: ts.Type | undefined;
try {
nodeType = checker.getTypeAtLocation(node);
declaration.typeSignature = checker.typeToString(nodeType);
} catch (err) {
declaration.partial = true;
const loc = getNodeLocation(node);
diagnostics.push({
kind: 'type_extraction_failed',
file: loc.file,
line: loc.line,
column: loc.column,
message: `Failed to extract type for "${declaration.name}": ${err instanceof Error ? err.message : String(err)}`,
severity: 'warning',
symbolName: declaration.name ?? '<default export>',
});
}
if (ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node)) {
if (node.typeParameters?.length) {
declaration.genericParams = node.typeParameters.map(parseGenericParam);
}
}
if (ts.isTypeAliasDeclaration(node) && nodeType) {
extractTypeAliasProperties(node, nodeType, checker, declaration, diagnostics, isExternalFile);
}
if (ts.isInterfaceDeclaration(node)) {
if (node.heritageClauses) {
declaration.extends = node.heritageClauses
.filter((hc) => hc.token === ts.SyntaxKind.ExtendsKeyword)
.flatMap((hc) => hc.types.map((t) => t.getText()));
}
// Extract properties and method signatures with full metadata
const processedMethods: Set<string> = new Set();
for (const member of node.members) {
if (ts.isPropertySignature(member) && ts.isIdentifier(member.name)) {
const propName = member.name.text;
const propDeclaration: MemberJsonBuild = {
name: propName,
kind: 'variable',
};
if (member.questionToken) {
propDeclaration.optional = true;
}
// Extract modifiers
const modifierFlags = extractModifiers(ts.getModifiers(member));
if (modifierFlags.length > 0) {
propDeclaration.modifiers = modifierFlags;
}
// Extract type
if (member.type) {
propDeclaration.typeSignature = member.type.getText();
}
// Extract TSDoc (applies docComment, examples, deprecated, seeAlso, since)
const propTsdoc = parseComment(member, node.getSourceFile());
applyToDeclaration(propDeclaration, propTsdoc);
(declaration.members ??= []).push(propDeclaration);
} else if (ts.isMethodSignature(member) && member.name) {
const methodName = ts.isIdentifier(member.name) ? member.name.text : member.name.getText();
if (!methodName || processedMethods.has(methodName)) continue;
processedMethods.add(methodName);
const methodDeclaration: MemberJsonBuild = {
name: methodName,
kind: 'function',
};
if (member.questionToken) {
methodDeclaration.optional = true;
}
// Extract modifiers
const modifierFlags = extractModifiers(ts.getModifiers(member));
if (modifierFlags.length > 0) {
methodDeclaration.modifiers = modifierFlags;
}
// Extract generic type parameters
if (member.typeParameters?.length) {
methodDeclaration.genericParams = member.typeParameters.map(parseGenericParam);
}
// Extract TSDoc
const methodTsdoc = parseComment(member, node.getSourceFile());
applyToDeclaration(methodDeclaration, methodTsdoc);
// Extract signatures via type checker
try {
const memberSymbol = checker.getSymbolAtLocation(member.name);
if (memberSymbol) {
const memberType = checker.getTypeOfSymbolAtLocation(memberSymbol, member);
populateCallableMember(
methodDeclaration,
memberType.getCallSignatures(),
checker,
methodTsdoc,
member,
methodName,
diagnostics,
);
}
} catch (err) {
methodDeclaration.partial = true;
const loc = getNodeLocation(member);
diagnostics.push({
kind: 'signature_analysis_failed',
file: loc.file,
line: loc.line,
column: loc.column,
message: `Failed to analyze interface method "${methodName}": ${err instanceof Error ? err.message : String(err)}`,
severity: 'warning',
functionName: methodName,
});
}
(declaration.members ??= []).push(methodDeclaration);
} else if (ts.isIndexSignatureDeclaration(member)) {
const param = member.parameters[0];
if (param && ts.isIdentifier(param.name) && param.type) {
const keyType = param.type.getText();
const name = `[${param.name.text}: ${keyType}]`;
const indexDeclaration: MemberJsonBuild = {name, kind: 'variable'};
if (member.type) {
indexDeclaration.typeSignature = member.type.getText();
}
const indexTsdoc = parseComment(member, node.getSourceFile());
applyToDeclaration(indexDeclaration, indexTsdoc);
(declaration.members ??= []).push(indexDeclaration);
}
}
}
// Extract call and construct signatures from interface type. TSDoc comes
// from inline signature declarations on this interface — inherited
// signatures resolve through `getCallSignatures()` but their docs are
// intentionally not surfaced here.
const interfaceType = nodeType ?? checker.getTypeAtLocation(node);
const errorContext = {node, kindLabel: 'interface'};
emitCallOrConstructSignature(
() => interfaceType.getCallSignatures(),
'call',
() => node.members.find(ts.isCallSignatureDeclaration),
node,
declaration,
checker,
diagnostics,
errorContext,
);
emitCallOrConstructSignature(
() => interfaceType.getConstructSignatures(),
'construct',
() => node.members.find(ts.isConstructSignatureDeclaration),
node,
declaration,
checker,
diagnostics,
errorContext,
);
}
};
/**
* Extract enum member information from an enum declaration.
*
* Iterates `node.members` to extract each enum member's name, initializer value,
* type, and JSDoc. Members are represented as `MemberJson` with kind `'variable'`.
*
* @internal Used by `analyzeDeclaration` — not part of the public barrel export.
*
* @mutates declaration - adds members and typeSignature
*/
export const extractEnumInfo = (
node: ts.Node,
checker: ts.TypeChecker,
declaration: DeclarationJsonBuild,
diagnostics: Array<Diagnostic>,
): void => {
// Extract type signature
try {
const nodeType = checker.getTypeAtLocation(node);
declaration.typeSignature = checker.typeToString(nodeType);
} catch (err) {
declaration.partial = true;
const loc = getNodeLocation(node);
diagnostics.push({
kind: 'type_extraction_failed',
file: loc.file,
line: loc.line,
column: loc.column,
message: `Failed to extract type for "${declaration.name}": ${err instanceof Error ? err.message : String(err)}`,
severity: 'warning',
symbolName: declaration.name ?? '<default export>',
});
}
if (!ts.isEnumDeclaration(node)) return;
for (const member of node.members) {
const memberName = ts.isIdentifier(member.name)
? member.name.text
: ts.isStringLiteral(member.name)
? member.name.text
: member.name.getText();
if (!memberName) continue;
const memberDeclaration: MemberJsonBuild = {
name: memberName,
kind: 'variable',
};
// Extract TSDoc
const memberTsdoc = parseComment(member, node.getSourceFile());
applyToDeclaration(memberDeclaration, memberTsdoc);
// Extract type and value via the checker
try {
const memberSymbol = checker.getSymbolAtLocation(member.name);
if (memberSymbol) {
const memberType = checker.getTypeOfSymbolAtLocation(memberSymbol, member);
memberDeclaration.typeSignature = checker.typeToString(memberType);
}
} catch (err) {
memberDeclaration.partial = true;
const loc = getNodeLocation(member);
diagnostics.push({
kind: 'type_extraction_failed',
file: loc.file,
line: loc.line,
column: loc.column,
message: `Failed to extract type for enum member "${memberName}" in "${declaration.name}": ${err instanceof Error ? err.message : String(err)}`,
severity: 'warning',
symbolName: memberName,
});
}
(declaration.members ??= []).push(memberDeclaration);
}
};
{
"path": "typescript-extract-type.ts",
"declarations": [
{
"name": "extractTypeInfo",
"kind": "function",
"docComment": "Extract type/interface information with rich property metadata.",
"typeSignature": "(node: Node, checker: TypeChecker, declaration: DeclarationJsonBuild, diagnostics: ({ symbolName: string; file: string; message: string; severity: \"error\" | \"warning\"; kind: \"type_extraction_failed\"; line?: number | undefined; column?: number | undefined; } | ... 12 more ... | { ...; })[], isExternalFile: IsExternalFile): void",
"sourceLine": 48,
"mutates": {
"declaration": "adds typeSignature, genericParams, extends, intersects, members (and `partial: true` on extraction failure)",
"diagnostics": "adds `type_extraction_failed` / `signature_analysis_failed` diagnostics on checker errors"
},
"parameters": [
{
"name": "node",
"type": "Node",
"description": "the declaration AST node"
},
{
"name": "checker",
"type": "TypeChecker",
"description": "TypeScript type checker"
},
{
"name": "declaration",
"type": "DeclarationJsonBuild",
"description": "the declaration to populate"
},
{
"name": "diagnostics",
"type": "({ symbolName: string; file: string; message: string; severity: \"error\" | \"warning\"; kind: \"type_extraction_failed\"; line?: number | undefined; column?: number | undefined; } | { functionName: string; ... 5 more ...; column?: number | undefined; } | ... 11 more ... | { ...; })[]",
"description": "diagnostics collector for non-fatal issues"
},
{
"name": "isExternalFile",
"type": "IsExternalFile"
}
],
"returnType": "void"
},
{
"name": "extractEnumInfo",
"kind": "function",
"docComment": "Extract enum member information from an enum declaration.\n\nIterates `node.members` to extract each enum member's name, initializer value,\ntype, and JSDoc. Members are represented as `MemberJson` with kind `'variable'`.",
"typeSignature": "(node: Node, checker: TypeChecker, declaration: DeclarationJsonBuild, diagnostics: ({ symbolName: string; file: string; message: string; severity: \"error\" | \"warning\"; kind: \"type_extraction_failed\"; line?: number | undefined; column?: number | undefined; } | ... 12 more ... | { ...; })[]): void",
"sourceLine": 238,
"mutates": {
"declaration": "adds members and typeSignature"
},
"parameters": [
{
"name": "node",
"type": "Node"
},
{
"name": "checker",
"type": "TypeChecker"
},
{
"name": "declaration",
"type": "DeclarationJsonBuild"
},
{
"name": "diagnostics",
"type": "({ symbolName: string; file: string; message: string; severity: \"error\" | \"warning\"; kind: \"type_extraction_failed\"; line?: number | undefined; column?: number | undefined; } | { functionName: string; ... 5 more ...; column?: number | undefined; } | ... 11 more ... | { ...; })[]"
}
],
"returnType": "void"
}
],
"moduleComment": "Per-declaration extractors for TypeScript type aliases, interfaces, and enums.\n\n`extractTypeInfo` handles type aliases and interfaces (including index/method/\ncall/construct signatures and intersection filtering). `extractEnumInfo`\nhandles regular and const enums. Both mutate a `DeclarationJsonBuild` with\nrich metadata derived from the TypeScript checker. Called by\n`analyzeDeclaration` in `typescript-exports.ts` once kind dispatch is\nsettled.\n\nType-alias property walking (named properties, index/call/construct\nsignatures) lives in `typescript-extract-type-properties.ts` to keep this\nmodule focused on top-level dispatch.\n\n@see `typescript-extract-shared.ts` for shared helpers\n@see `typescript-extract-type-properties.ts` for type-alias property walking",
"dependencies": [
"declaration-build.ts",
"diagnostics.ts",
"tsdoc.ts",
"typescript-extract-shared.ts",
"typescript-extract-type-properties.ts",
"typescript-program.ts"
],
"dependents": [
"typescript-exports.ts"
]
}