Skip to content
2 changes: 1 addition & 1 deletion src/parsers/exportParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export function parseExport(
type: ts.Type,
parentNamespaces: string[],
) {
const parsedType = resolveType(type, symbol.getName(), parserContext);
const parsedType = resolveType(type, parserContext);
if (parsedType) {
// Patch parentNamespaces if the type supports it
if (parsedType && 'parentNamespaces' in parsedType) {
Expand Down
9 changes: 3 additions & 6 deletions src/parsers/functionParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,7 @@ function parseFunctionSignature(
parseParameter(parameterSymbol, context, skipResolvingComplexTypes),
);

const returnValueType = resolveType(
signature.getReturnType(),
signature.getDeclaration().name?.getText() || '',
context,
);
const returnValueType = resolveType(signature.getReturnType(), context);

return new CallSignature(parameters, returnValueType);
}
Expand All @@ -94,10 +90,11 @@ function parseParameter(

try {
const parameterDeclaration = parameterSymbol.valueDeclaration as ts.ParameterDeclaration;

const parameterType = resolveType(
checker.getTypeOfSymbolAtLocation(parameterSymbol, parameterSymbol.valueDeclaration!),
parameterSymbol.getName(),
context,
parameterDeclaration.type,
skipResolvingComplexTypes,
);

Expand Down
13 changes: 8 additions & 5 deletions src/parsers/objectParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { getTypeNamespaces } from './typeResolver';

export function parseObjectType(
type: ts.Type,
name: string,
context: ParserContext,
skipResolvingComplexTypes: boolean,
): ObjectNode | undefined {
Expand All @@ -25,9 +24,13 @@ export function parseObjectType(
if (properties.length) {
if (
!skipResolvingComplexTypes &&
shouldResolveObject({ name, propertyCount: properties.length, depth: typeStack.length })
shouldResolveObject({
name: typeName ?? '',
propertyCount: properties.length,
depth: typeStack.length,
})
) {
const filtered = properties.filter((property) => {
const filteredProperties = properties.filter((property) => {
const declaration =
property.valueDeclaration ??
(property.declarations?.[0] as ts.PropertySignature | undefined);
Expand All @@ -37,11 +40,11 @@ export function parseObjectType(
shouldInclude({ name: property.getName(), depth: typeStack.length + 1 })
);
});
if (filtered.length > 0) {
if (filteredProperties.length > 0) {
return new ObjectNode(
typeName,
getTypeNamespaces(type),
filtered.map((property) => {
filteredProperties.map((property) => {
return parseProperty(
property,
property.valueDeclaration as ts.PropertySignature,
Expand Down
36 changes: 32 additions & 4 deletions src/parsers/propertyParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ export function parseProperty(

try {
let type: ts.Type;

if (propertySignature) {
if (!propertySignature.type) {
type = checker.getAnyType();
} else {
if (propertySignature.type) {
type = checker.getTypeOfSymbolAtLocation(propertySymbol, propertySignature.type);
} else {
type = checker.getAnyType();
}
} else {
type = checker.getTypeOfSymbol(propertySymbol);
Expand All @@ -36,7 +37,12 @@ export function parseProperty(
parsedType = new IntrinsicNode('any');
isOptional = Boolean(propertySignature.questionToken);
} else {
parsedType = resolveType(type, propertySymbol.getName(), context, skipResolvingComplexTypes);
parsedType = resolveType(
type,
context,
isTypeParameterLike(type) ? undefined : propertySignature?.type,
skipResolvingComplexTypes,
);
isOptional = Boolean(propertySymbol.flags & ts.SymbolFlags.Optional);
}

Expand All @@ -58,3 +64,25 @@ export function parseProperty(
parsedSymbolStack.pop();
}
}

function isTypeParameterLike(type: ts.Type): boolean {
// Check if the type is a type parameter
return (
(type.flags & ts.TypeFlags.TypeParameter) !== 0 ||
((type.flags & ts.TypeFlags.Union) !== 0 && isOptionalTypeParameter(type as ts.UnionType))
);
}

function isOptionalTypeParameter(type: ts.UnionType): boolean {
// Check if the type is defined as
// foo?: T
// where T is a type parameter

return (
type.types.length === 2 &&
type.types.some((t) => t.flags & ts.TypeFlags.Undefined) &&
type.types.some(
(t) => 'objectFlags' in t && ((t.objectFlags as number) & ts.ObjectFlags.Instantiated) !== 0,
)
);
}
95 changes: 58 additions & 37 deletions src/parsers/typeResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import {

export function resolveType(
type: ts.Type,
name: string,
context: ParserContext,
typeNode?: ts.TypeNode,
skipResolvingComplexTypes: boolean = false,
): TypeNode {
const { checker, typeStack, includeExternalTypes } = context;
Expand All @@ -37,7 +37,23 @@ export function resolveType(
typeStack.push(typeId);
}

const namespaces = getTypeNamespaces(type);
const typeNodeSymbol =
typeNode && ts.isTypeReferenceNode(typeNode)
? checker.getSymbolAtLocation((typeNode as ts.TypeReferenceNode).typeName)
: undefined;
const namespaces = typeNodeSymbol
? getTypeSymbolNamespaces(typeNodeSymbol)
: getTypeNamespaces(type);

let typeSymbol: ts.Symbol | undefined;
if (typeNode && ts.isTypeReferenceNode(typeNode)) {
const typeNodeName = (typeNode as ts.TypeReferenceNode).typeName;
if (ts.isIdentifier(typeNodeName)) {
typeSymbol = checker.getSymbolAtLocation(typeNodeName);
} else if (ts.isQualifiedName(typeNodeName)) {
typeSymbol = checker.getSymbolAtLocation(typeNodeName.right);
}
}

try {
if (type.flags & ts.TypeFlags.TypeParameter && type.symbol) {
Expand All @@ -47,29 +63,25 @@ export function resolveType(
namespaces,
declaration?.constraint?.getText(),
declaration?.default
? resolveType(checker.getTypeAtLocation(declaration.default), '', context)
? resolveType(checker.getTypeAtLocation(declaration.default), context)
: undefined,
);
}

if (checker.isArrayType(type)) {
// @ts-expect-error - Private method
const arrayType: ts.Type = checker.getElementTypeOfArrayType(type);
return new ArrayNode(
type.aliasSymbol?.name,
namespaces,
resolveType(arrayType, name, context),
);
return new ArrayNode(type.aliasSymbol?.name, namespaces, resolveType(arrayType, context));
}

if (!includeExternalTypes && isTypeExternal(type, checker)) {
const typeName = getTypeName(type, checker);
const typeName = getTypeName(type, typeSymbol, checker);
// Fixes a weird TS behavior where it doesn't show the alias name but resolves to the actual type in case of RefCallback.
if (typeName === 'bivarianceHack') {
return new ReferenceNode('RefCallback', []);
}

return new ReferenceNode(getTypeName(type, checker), namespaces);
return new ReferenceNode(getTypeName(type, typeSymbol, checker), namespaces);
}

if (hasFlag(type.flags, ts.TypeFlags.Boolean)) {
Expand All @@ -96,7 +108,7 @@ export function resolveType(

if (type.isUnion()) {
const memberTypes: TypeNode[] = [];
const symbol = type.aliasSymbol ?? type.getSymbol();
const symbol = typeSymbol ?? type.aliasSymbol ?? type.getSymbol();
let typeName = symbol?.getName();
if (typeName === '__type') {
typeName = undefined;
Expand All @@ -106,11 +118,11 @@ export function resolveType(
if (type.origin?.isUnion()) {
// @ts-expect-error - Internal API
for (const memberType of type.origin.types) {
memberTypes.push(resolveType(memberType, memberType.getSymbol()?.name || '', context));
memberTypes.push(resolveType(memberType, context));
}
} else {
for (const memberType of type.types) {
memberTypes.push(resolveType(memberType, memberType.getSymbol()?.name || '', context));
memberTypes.push(resolveType(memberType, context));
}
}

Expand All @@ -121,14 +133,14 @@ export function resolveType(

if (type.isIntersection()) {
const memberTypes: TypeNode[] = [];
const symbol = type.aliasSymbol ?? type.getSymbol();
const symbol = typeSymbol ?? type.aliasSymbol ?? type.getSymbol();
let typeName = symbol?.getName();
if (typeName === '__type') {
typeName = undefined;
}

for (const memberType of type.types) {
memberTypes.push(resolveType(memberType, memberType.getSymbol()?.name || '', context));
memberTypes.push(resolveType(memberType, context));
}

if (memberTypes.length === 0) {
Expand All @@ -149,7 +161,7 @@ export function resolveType(
return parseFunctionType(type, context)!;
}

const objectType = parseObjectType(type, name, context, skipResolvingComplexTypes);
const objectType = parseObjectType(type, context, skipResolvingComplexTypes);
if (objectType) {
return new IntersectionNode(typeName, namespaces, memberTypes, objectType.properties);
}
Expand All @@ -160,11 +172,9 @@ export function resolveType(

if (checker.isTupleType(type)) {
return new TupleNode(
undefined,
typeSymbol?.name ?? type.aliasSymbol?.name,
[],
(type as ts.TupleType).typeArguments?.map((x) =>
resolveType(x, x.getSymbol()?.name || '', context),
) ?? [],
(type as ts.TupleType).typeArguments?.map((x) => resolveType(x, context)) ?? [],
);
}

Expand Down Expand Up @@ -218,7 +228,7 @@ export function resolveType(
return parseFunctionType(type, context)!;
}

const objectType = parseObjectType(type, name, context, skipResolvingComplexTypes);
const objectType = parseObjectType(type, context, skipResolvingComplexTypes);
if (objectType) {
return objectType;
}
Expand All @@ -244,14 +254,14 @@ export function resolveType(
undefined,
[],
[
resolveType((type as ts.ConditionalType).resolvedTrueType!, '', context),
resolveType((type as ts.ConditionalType).resolvedFalseType!, '', context),
resolveType((type as ts.ConditionalType).resolvedTrueType!, context),
resolveType((type as ts.ConditionalType).resolvedFalseType!, context),
],
);
} else if (conditionalType.resolvedTrueType) {
return resolveType(conditionalType.resolvedTrueType, '', context);
return resolveType(conditionalType.resolvedTrueType, context);
} else if (conditionalType.resolvedFalseType) {
return resolveType(conditionalType.resolvedFalseType, '', context);
return resolveType(conditionalType.resolvedFalseType, context);
}
}

Expand Down Expand Up @@ -291,24 +301,31 @@ export function getTypeNamespaces(type: ts.Type): string[] {
return [];
}

if (symbol.name === '__function' || symbol.name === '__type') {
return getTypeSymbolNamespaces(symbol);
}

function getTypeSymbolNamespaces(typeSymbol: ts.Symbol): string[] {
if (typeSymbol.name === '__function' || typeSymbol.name === '__type') {
return [];
}

const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
if (!declaration) {
const declaration = typeSymbol.valueDeclaration ?? typeSymbol.declarations?.[0];
return getNodeNamespaces(declaration);
}
export function getNodeNamespaces(node: ts.Node | undefined): string[] {
if (!node) {
return [];
}

const namespaces: string[] = [];
let currentDeclaration: ts.Node = declaration.parent;
let currentNode = node.parent;

while (currentDeclaration != null && !ts.isSourceFile(currentDeclaration)) {
if (ts.isModuleDeclaration(currentDeclaration)) {
namespaces.unshift(currentDeclaration.name.getText());
while (currentNode != null && !ts.isSourceFile(currentNode)) {
if (ts.isModuleDeclaration(currentNode)) {
namespaces.unshift(currentNode.name.getText());
}

currentDeclaration = currentDeclaration.parent;
currentNode = currentNode.parent;
}

return namespaces;
Expand All @@ -333,8 +350,12 @@ function isTypeExternal(type: ts.Type, checker: ts.TypeChecker): boolean {
);
}

function getTypeName(type: ts.Type, checker: ts.TypeChecker): string {
const symbol = type.aliasSymbol ?? type.getSymbol();
function getTypeName(
type: ts.Type,
typeSymbol: ts.Symbol | undefined,
checker: ts.TypeChecker,
): string {
const symbol = typeSymbol ?? type.aliasSymbol ?? type.getSymbol();
if (!symbol) {
return checker.typeToString(type);
}
Expand All @@ -348,11 +369,11 @@ function getTypeName(type: ts.Type, checker: ts.TypeChecker): string {
if ('target' in type) {
typeArguments = checker
.getTypeArguments(type as ts.TypeReference)
?.map((x) => getTypeName(x, checker));
?.map((x) => getTypeName(x, undefined, checker));
}

if (!typeArguments?.length) {
typeArguments = type.aliasTypeArguments?.map((x) => getTypeName(x, checker)) ?? [];
typeArguments = type.aliasTypeArguments?.map((x) => getTypeName(x, undefined, checker)) ?? [];
}

if (typeArguments && typeArguments.length > 0) {
Expand Down
10 changes: 10 additions & 0 deletions test/aliases/input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface A {
a: Alias;
r: MyRecord<string, string>;
}

export function fn1(a: Alias, r: MyRecord<string, string>) {}

type SomeType = 1 | 2;
type Alias = SomeType;
type MyRecord<Key extends string | number, Value> = Record<Key, Value>;
Loading