diff --git a/CHANGELOG.md b/CHANGELOG.md index 37694ccbf..0a309cb5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Unreleased +### Features + +- TypeDoc will now treat `@typedef {import("foo").Bar} Baz` type declarations which forward type parameters to the imported + symbol as re-exports of that symbol, #2044. + ## v0.23.14 (2022-09-03) ### Features diff --git a/src/lib/converter/jsdoc.ts b/src/lib/converter/jsdoc.ts index 9f6ace5a6..f717b330d 100644 --- a/src/lib/converter/jsdoc.ts +++ b/src/lib/converter/jsdoc.ts @@ -18,6 +18,7 @@ import { convertParameterNodes, convertTypeParameterNodes, } from "./factories/signature"; +import { convertSymbol } from "./symbols"; export function convertJsDocAlias( context: Context, @@ -33,6 +34,15 @@ export function convertJsDocAlias( return; } + // If the typedef tag is just referring to another type-space symbol, with no type parameters + // or appropriate forwarding type parameters, then we treat it as a re-export instead of creating + // a type alias with an import type. + const aliasedSymbol = getTypedefReExportTarget(context, declaration); + if (aliasedSymbol) { + convertSymbol(context, aliasedSymbol, exportSymbol ?? symbol); + return; + } + const reflection = context.createDeclarationReflection( ReflectionKind.TypeAlias, symbol, @@ -165,3 +175,58 @@ function convertTemplateParameterNodes( const params = (nodes ?? []).flatMap((tag) => tag.typeParameters); return convertTypeParameterNodes(context, params); } + +function getTypedefReExportTarget( + context: Context, + declaration: ts.JSDocTypedefTag | ts.JSDocEnumTag +): ts.Symbol | undefined { + const typeExpression = declaration.typeExpression; + if ( + !ts.isJSDocTypedefTag(declaration) || + !typeExpression || + ts.isJSDocTypeLiteral(typeExpression) || + !ts.isImportTypeNode(typeExpression.type) || + !typeExpression.type.qualifier || + !ts.isIdentifier(typeExpression.type.qualifier) + ) { + return; + } + + const targetSymbol = context.expectSymbolAtLocation( + typeExpression.type.qualifier + ); + const decl = targetSymbol.declarations?.[0]; + + if ( + !decl || + !( + ts.isTypeAliasDeclaration(decl) || + ts.isInterfaceDeclaration(decl) || + ts.isJSDocTypedefTag(decl) || + ts.isJSDocCallbackTag(decl) + ) + ) { + return; + } + + const targetParams = ts.getEffectiveTypeParameterDeclarations(decl); + const localParams = ts.getEffectiveTypeParameterDeclarations(declaration); + const localArgs = typeExpression.type.typeArguments || []; + + // If we have type parameters, ensure they are forwarding parameters with no transformations. + // This doesn't check constraints since they aren't checked in JSDoc types. + if ( + targetParams.length !== localParams.length || + localArgs.some( + (arg, i) => + !ts.isTypeReferenceNode(arg) || + !ts.isIdentifier(arg.typeName) || + arg.typeArguments || + localParams[i]?.name.text !== arg.typeName.text + ) + ) { + return; + } + + return targetSymbol; +} diff --git a/src/lib/converter/types.ts b/src/lib/converter/types.ts index ec342011a..cbd02af1c 100644 --- a/src/lib/converter/types.ts +++ b/src/lib/converter/types.ts @@ -37,6 +37,7 @@ import { createSignature, } from "./factories/signature"; import { convertSymbol } from "./symbols"; +import { isObjectType } from "./utils/nodes"; import { removeUndefined } from "./utils/reflections"; export interface TypeConverter< @@ -1035,10 +1036,6 @@ function requestBugReport(context: Context, nodeOrType: ts.Node | ts.Type) { } } -function isObjectType(type: ts.Type): type is ts.ObjectType { - return typeof (type as any).objectFlags === "number"; -} - function resolveReference(type: ts.Type) { if (isObjectType(type) && type.objectFlags & ts.ObjectFlags.Reference) { return (type as ts.TypeReference).target; diff --git a/src/lib/converter/utils/nodes.ts b/src/lib/converter/utils/nodes.ts index c0cff59dd..ab6302276 100644 --- a/src/lib/converter/utils/nodes.ts +++ b/src/lib/converter/utils/nodes.ts @@ -30,3 +30,7 @@ export function getHeritageTypes( return true; }); } + +export function isObjectType(type: ts.Type): type is ts.ObjectType { + return typeof (type as any).objectFlags === "number"; +} diff --git a/src/test/converter2.test.ts b/src/test/converter2.test.ts index a4d1709f8..c22c67f47 100644 --- a/src/test/converter2.test.ts +++ b/src/test/converter2.test.ts @@ -30,6 +30,7 @@ function runTest( join(base, `${entry}.tsx`), join(base, `${entry}.js`), join(base, entry, "index.ts"), + join(base, entry, "index.js"), ].find(existsSync); ok(entryPoint, `No entry point found for ${entry}`); diff --git a/src/test/converter2/issues/gh1896.js b/src/test/converter2/issues/gh1896.js index 551a787c9..7a7eda324 100644 --- a/src/test/converter2/issues/gh1896.js +++ b/src/test/converter2/issues/gh1896.js @@ -8,7 +8,7 @@ /** * Before tag - * @typedef {{(one: number, two: number) => number}} Type2 + * @typedef {{(one: number, two: number): number}} Type2 * * Some type 2. */ diff --git a/src/test/converter2/issues/gh2044/index.js b/src/test/converter2/issues/gh2044/index.js new file mode 100644 index 000000000..62c3c0da0 --- /dev/null +++ b/src/test/converter2/issues/gh2044/index.js @@ -0,0 +1,18 @@ +export { other } from "./other"; + +/** @typedef {import("./other").Foo} Foo */ +/** @typedef {import("./other").Foo} RenamedFoo */ + +/** + * @typedef {import("./other").Generic} Generic + * @template T + */ + +/** + * @typedef {import("./other").Generic} RenamedGeneric + * @template {string} U + */ + +/** + * @typedef {import("./other").Generic} NonGeneric + */ diff --git a/src/test/converter2/issues/gh2044/other.js b/src/test/converter2/issues/gh2044/other.js new file mode 100644 index 000000000..253b911ca --- /dev/null +++ b/src/test/converter2/issues/gh2044/other.js @@ -0,0 +1,8 @@ +/** @typedef {string} Foo */ + +/** + * @typedef {T extends `a${infer F}` ? F : never} Generic + * @template {string} T + */ + +export const other = 123; diff --git a/src/test/converter2/tsconfig.json b/src/test/converter2/tsconfig.json index 079cd2b4d..65b24c4a5 100644 --- a/src/test/converter2/tsconfig.json +++ b/src/test/converter2/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "strict": true, "module": "CommonJS", - "allowJs": true, + "checkJs": true, "outDir": "dist", "target": "ESNext", diff --git a/src/test/issueTests.ts b/src/test/issueTests.ts index 2a94871ec..da18d79ac 100644 --- a/src/test/issueTests.ts +++ b/src/test/issueTests.ts @@ -16,6 +16,7 @@ import { UnionType, LiteralType, IntrinsicType, + ReferenceReflection, } from "../lib/models"; import type { InlineTagDisplayPart } from "../lib/models/comments/comment"; import { getConverter2App } from "./programs"; @@ -766,4 +767,17 @@ export const issueTests: { equal(MultipleSimpleCtors.type.declaration.signatures?.length, 2); equal(AnotherCtor.type.declaration.signatures?.length, 1); }, + + gh2044(project) { + for (const [name, ref] of [ + ["Foo", false], + ["RenamedFoo", true], + ["Generic", false], + ["RenamedGeneric", true], + ["NonGeneric", false], + ] as const) { + const decl = query(project, name); + equal(decl instanceof ReferenceReflection, ref, `${name} = ${ref}`); + } + }, };