Skip to content

Commit dbefdbd

Browse files
committed
Recognize and respect type-only exports
Resolves #2962
1 parent a2f0d16 commit dbefdbd

File tree

5 files changed

+92
-2
lines changed

5 files changed

+92
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ title: Changelog
1212

1313
- Attempting to highlight a supported language which is not enabled is now a warning, not an error, #2956.
1414
- Improved compatibility with CommonMark's link parsing, #2959.
15+
- Classes, variables, and functions exported with `export { type X }` are now detected and converted as interfaces/type aliases, #2962.
1516

1617
## v0.28.5 (2025-05-26)
1718

src/lib/converter/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ export class Context {
190190
exportSymbol: ts.Symbol | undefined,
191191
) {
192192
if (
193+
!reflection.comment &&
193194
exportSymbol &&
194195
reflection.kind &
195196
(ReflectionKind.SomeModule | ReflectionKind.Reference)

src/lib/converter/symbols.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,42 @@ function convertTypeAlias(
409409
}
410410
}
411411

412+
function convertTypeAliasFromValueDeclaration(
413+
context: Context,
414+
symbol: ts.Symbol,
415+
exportSymbol: ts.Symbol | undefined,
416+
valueKind: ReflectionKind,
417+
): undefined {
418+
const comment = context.getComment(symbol, valueKind);
419+
420+
const reflection = new DeclarationReflection(
421+
exportSymbol?.name || symbol.name,
422+
ReflectionKind.TypeAlias,
423+
context.scope,
424+
);
425+
reflection.comment = comment;
426+
context.postReflectionCreation(reflection, symbol, exportSymbol);
427+
context.finalizeDeclarationReflection(reflection);
428+
429+
reflection.type = context.converter.convertType(
430+
context.withScope(reflection),
431+
context.checker.getTypeOfSymbol(symbol),
432+
);
433+
434+
if (reflection.type.type === "reflection" && reflection.type.declaration.children) {
435+
// #2817 lift properties of object literal types up to the reflection level.
436+
const typeDecl = reflection.type.declaration;
437+
reflection.project.mergeReflections(typeDecl, reflection);
438+
delete reflection.type;
439+
440+
// When created any signatures will be created with __type as their
441+
// name, rename them so that they have the alias's name as their name
442+
for (const sig of reflection.signatures || []) {
443+
sig.name = reflection.name;
444+
}
445+
}
446+
}
447+
412448
function attachUnionComments(
413449
context: Context,
414450
declaration: ts.TypeAliasDeclaration,
@@ -495,6 +531,10 @@ function convertFunctionOrMethod(
495531
symbol: ts.Symbol,
496532
exportSymbol?: ts.Symbol,
497533
): undefined | ts.SymbolFlags {
534+
if (isTypeOnlyExport(exportSymbol)) {
535+
return convertTypeAliasFromValueDeclaration(context, symbol, exportSymbol, ReflectionKind.Function);
536+
}
537+
498538
// Can't just check method flag because this might be called for properties as well
499539
// This will *NOT* be called for variables that look like functions, they need a special case.
500540
const isMethod = !!(
@@ -573,7 +613,7 @@ function convertClassOrInterface(
573613
exportSymbol?: ts.Symbol,
574614
) {
575615
const reflection = context.createDeclarationReflection(
576-
ts.SymbolFlags.Class & symbol.flags
616+
(ts.SymbolFlags.Class & symbol.flags) && !isTypeOnlyExport(exportSymbol)
577617
? ReflectionKind.Class
578618
: ReflectionKind.Interface,
579619
symbol,
@@ -618,7 +658,7 @@ function convertClassOrInterface(
618658

619659
context.finalizeDeclarationReflection(reflection);
620660

621-
if (classDeclaration) {
661+
if (classDeclaration && reflection.kind === ReflectionKind.Class) {
622662
// Classes can have static props
623663
const staticType = context.checker.getTypeOfSymbolAtLocation(
624664
symbol,
@@ -984,6 +1024,10 @@ function convertVariable(
9841024
symbol: ts.Symbol,
9851025
exportSymbol?: ts.Symbol,
9861026
): undefined | ts.SymbolFlags {
1027+
if (isTypeOnlyExport(exportSymbol)) {
1028+
return convertTypeAliasFromValueDeclaration(context, symbol, exportSymbol, ReflectionKind.Variable);
1029+
}
1030+
9871031
const declaration = symbol.getDeclarations()?.[0];
9881032

9891033
const comment = context.getComment(symbol, ReflectionKind.Variable);
@@ -1485,3 +1529,13 @@ function isFunctionLikeInitializer(node: ts.Expression): boolean {
14851529

14861530
return false;
14871531
}
1532+
1533+
function isTypeOnlyExport(symbol: ts.Symbol | undefined): boolean {
1534+
if (!symbol) return false;
1535+
1536+
const declaration = symbol.declarations?.[0];
1537+
if (!declaration) return false;
1538+
if (!ts.isExportSpecifier(declaration)) return false;
1539+
1540+
return declaration.isTypeOnly || declaration.parent.parent.isTypeOnly;
1541+
}

src/test/converter2/issues/gh2962.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
class Class {
2+
msg: string;
3+
4+
constructor(msg: string) {
5+
this.msg = msg;
6+
}
7+
}
8+
9+
const Var = 123;
10+
11+
function Func<T>(a: T) {}
12+
13+
export { type Class, type Func, type Var };
14+
15+
class Class2 {}
16+
const Var2 = 123;
17+
function Func2() {}
18+
export type { Class2, Func2, Var2 };

src/test/issues.c2.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2140,4 +2140,20 @@ describe("Issue Tests", () => {
21402140
equal(query(project, "InterfaceA.propertyB").type?.toString(), "AliasB<string>");
21412141
equal(query(project, "InterfaceA.propertyC").type?.toString(), "AliasC");
21422142
});
2143+
2144+
it("#2962 handles type-only exports", () => {
2145+
const project = convert();
2146+
equal(project.children?.map(c => [c.name, ReflectionKind[c.kind]]), [
2147+
["Class", "Interface"],
2148+
["Class2", "Interface"],
2149+
["Func", "TypeAlias"],
2150+
["Func2", "TypeAlias"],
2151+
["Var", "TypeAlias"],
2152+
["Var2", "TypeAlias"],
2153+
]);
2154+
2155+
equal(query(project, "Class").children?.map(c => c.name), ["msg"]);
2156+
equal(query(project, "Func").type?.toString(), "(a: T) => void");
2157+
equal(query(project, "Var").type?.toString(), "123");
2158+
});
21432159
});

0 commit comments

Comments
 (0)