typedoc
Version:
Create api documentation for TypeScript projects.
789 lines (788 loc) • 34.7 kB
JavaScript
import assert from "assert";
import ts from "typescript";
import { ArrayType, ConditionalType, DeclarationReflection, IndexedAccessType, InferredType, IntersectionType, IntrinsicType, NamedTupleMember, PredicateType, QueryType, ReferenceType, ReflectionKind, ReflectionType, LiteralType, TupleType, TypeOperatorType, UnionType, UnknownType, MappedType, SignatureReflection, ReflectionFlag, OptionalType, RestType, TemplateLiteralType, } from "../models/index.js";
import { ReflectionSymbolId } from "../models/reflections/ReflectionSymbolId.js";
import { zip } from "../utils/array.js";
import { ConverterEvents } from "./converter-events.js";
import { convertIndexSignatures } from "./factories/index-signature.js";
import { convertParameterNodes, convertTypeParameterNodes, createSignature, } from "./factories/signature.js";
import { convertSymbol } from "./symbols.js";
import { isObjectType, isTypeReference } from "./utils/nodes.js";
import { removeUndefined } from "./utils/reflections.js";
const converters = new Map();
export function loadConverters() {
if (converters.size)
return;
for (const actor of [
arrayConverter,
conditionalConverter,
constructorConverter,
exprWithTypeArgsConverter,
functionTypeConverter,
importType,
indexedAccessConverter,
inferredConverter,
intersectionConverter,
intrinsicConverter,
jsDocVariadicTypeConverter,
keywordConverter,
optionalConverter,
parensConverter,
predicateConverter,
queryConverter,
typeLiteralConverter,
referenceConverter,
restConverter,
namedTupleMemberConverter,
mappedConverter,
literalTypeConverter,
templateLiteralConverter,
thisConverter,
tupleConverter,
typeOperatorConverter,
unionConverter,
// Only used if skipLibCheck: true
jsDocNullableTypeConverter,
jsDocNonNullableTypeConverter,
]) {
for (const key of actor.kind) {
if (key === undefined) {
// Might happen if running on an older TS version.
continue;
}
assert(!converters.has(key));
converters.set(key, actor);
}
}
}
// This ought not be necessary, but we need some way to discover recursively
// typed symbols which do not have type nodes. See the `recursive` symbol in the variables test.
const seenTypes = new Set();
function maybeConvertType(context, typeOrNode) {
if (!typeOrNode) {
return;
}
return convertType(context, typeOrNode);
}
let typeConversionDepth = 0;
export function convertType(context, typeOrNode) {
if (!typeOrNode) {
return new IntrinsicType("any");
}
if (typeConversionDepth > context.converter.maxTypeConversionDepth) {
return new UnknownType("...");
}
loadConverters();
if ("kind" in typeOrNode) {
const converter = converters.get(typeOrNode.kind);
if (converter) {
++typeConversionDepth;
const result = converter.convert(context, typeOrNode);
--typeConversionDepth;
return result;
}
return requestBugReport(context, typeOrNode);
}
// TS 4.2 added this to enable better tracking of type aliases.
// We need to check it here, not just in the union checker, because typeToTypeNode
// will use the origin when serializing
// aliasSymbol check is important - #2468
if (typeOrNode.isUnion() && typeOrNode.origin && !typeOrNode.aliasSymbol) {
// Don't increment typeConversionDepth as this is a transparent step to the user.
return convertType(context, typeOrNode.origin);
}
// IgnoreErrors is important, without it, we can't assert that we will get a node.
const node = context.checker.typeToTypeNode(typeOrNode, void 0, ts.NodeBuilderFlags.IgnoreErrors);
assert(node); // According to the TS source of typeToString, this is a bug if it does not hold.
if (seenTypes.has(typeOrNode.id)) {
const typeString = context.checker.typeToString(typeOrNode);
context.logger.verbose(`Refusing to recurse when converting type: ${typeString}`);
return new UnknownType(typeString);
}
let converter = converters.get(node.kind);
if (converter) {
// Hacky fix for #2011, need to find a better way to choose the converter.
if (converter === intersectionConverter &&
!typeOrNode.isIntersection()) {
converter = typeLiteralConverter;
}
seenTypes.add(typeOrNode.id);
++typeConversionDepth;
const result = converter.convertType(context, typeOrNode, node);
--typeConversionDepth;
seenTypes.delete(typeOrNode.id);
return result;
}
return requestBugReport(context, typeOrNode);
}
const arrayConverter = {
kind: [ts.SyntaxKind.ArrayType],
convert(context, node) {
return new ArrayType(convertType(context, node.elementType));
},
convertType(context, type) {
const params = context.checker.getTypeArguments(type);
// This is *almost* always true... except for when this type is in the constraint of a type parameter see GH#1408
// assert(params.length === 1);
assert(params.length > 0);
return new ArrayType(convertType(context, params[0]));
},
};
const conditionalConverter = {
kind: [ts.SyntaxKind.ConditionalType],
convert(context, node) {
return new ConditionalType(convertType(context, node.checkType), convertType(context, node.extendsType), convertType(context, node.trueType), convertType(context, node.falseType));
},
convertType(context, type) {
return new ConditionalType(convertType(context, type.checkType), convertType(context, type.extendsType), convertType(context, type.resolvedTrueType), convertType(context, type.resolvedFalseType));
},
};
const constructorConverter = {
kind: [ts.SyntaxKind.ConstructorType],
convert(context, node) {
const symbol = context.getSymbolAtLocation(node) ?? node.symbol;
const type = context.getTypeAtLocation(node);
if (!symbol || !type) {
return new IntrinsicType("Function");
}
const reflection = new DeclarationReflection("__type", ReflectionKind.Constructor, context.scope);
const rc = context.withScope(reflection);
rc.convertingTypeNode = true;
context.registerReflection(reflection, symbol);
context.converter.trigger(ConverterEvents.CREATE_DECLARATION, context, reflection);
const signature = new SignatureReflection("__type", ReflectionKind.ConstructorSignature, reflection);
// This is unfortunate... but seems the obvious place to put this with the current
// architecture. Ideally, this would be a property on a "ConstructorType"... but that
// needs to wait until TypeDoc 0.22 when making other breaking changes.
if (node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword)) {
signature.setFlag(ReflectionFlag.Abstract);
}
context.project.registerSymbolId(signature, new ReflectionSymbolId(symbol, node));
context.registerReflection(signature, void 0);
const signatureCtx = rc.withScope(signature);
reflection.signatures = [signature];
signature.type = convertType(signatureCtx, node.type);
signature.parameters = convertParameterNodes(signatureCtx, signature, node.parameters);
signature.typeParameters = convertTypeParameterNodes(signatureCtx, node.typeParameters);
return new ReflectionType(reflection);
},
convertType(context, type) {
const symbol = type.getSymbol();
if (!symbol) {
return new IntrinsicType("Function");
}
const reflection = new DeclarationReflection("__type", ReflectionKind.Constructor, context.scope);
context.registerReflection(reflection, symbol);
context.converter.trigger(ConverterEvents.CREATE_DECLARATION, context, reflection);
createSignature(context.withScope(reflection), ReflectionKind.ConstructorSignature, type.getConstructSignatures()[0], symbol);
return new ReflectionType(reflection);
},
};
const exprWithTypeArgsConverter = {
kind: [ts.SyntaxKind.ExpressionWithTypeArguments],
convert(context, node) {
const targetSymbol = context.getSymbolAtLocation(node.expression);
// Mixins... we might not have a symbol here.
if (!targetSymbol) {
return convertType(context, context.checker.getTypeAtLocation(node));
}
const parameters = node.typeArguments?.map((type) => convertType(context, type)) ?? [];
const ref = ReferenceType.createSymbolReference(context.resolveAliasedSymbol(targetSymbol), context);
ref.typeArguments = parameters;
return ref;
},
convertType: requestBugReport,
};
const functionTypeConverter = {
kind: [ts.SyntaxKind.FunctionType],
convert(context, node) {
const symbol = context.getSymbolAtLocation(node) ?? node.symbol;
const type = context.getTypeAtLocation(node);
if (!symbol || !type) {
return new IntrinsicType("Function");
}
const reflection = new DeclarationReflection("__type", ReflectionKind.TypeLiteral, context.scope);
const rc = context.withScope(reflection);
context.registerReflection(reflection, symbol);
context.converter.trigger(ConverterEvents.CREATE_DECLARATION, context, reflection);
const signature = new SignatureReflection("__type", ReflectionKind.CallSignature, reflection);
context.project.registerSymbolId(signature, new ReflectionSymbolId(symbol, node));
context.registerReflection(signature, undefined);
const signatureCtx = rc.withScope(signature);
reflection.signatures = [signature];
signature.type = convertType(signatureCtx, node.type);
signature.parameters = convertParameterNodes(signatureCtx, signature, node.parameters);
signature.typeParameters = convertTypeParameterNodes(signatureCtx, node.typeParameters);
return new ReflectionType(reflection);
},
convertType(context, type) {
const symbol = type.getSymbol();
if (!symbol) {
return new IntrinsicType("Function");
}
const reflection = new DeclarationReflection("__type", ReflectionKind.TypeLiteral, context.scope);
context.registerReflection(reflection, symbol);
context.converter.trigger(ConverterEvents.CREATE_DECLARATION, context, reflection);
createSignature(context.withScope(reflection), ReflectionKind.CallSignature, type.getCallSignatures()[0], type.getSymbol());
return new ReflectionType(reflection);
},
};
const importType = {
kind: [ts.SyntaxKind.ImportType],
convert(context, node) {
const name = node.qualifier?.getText() ?? "__module";
const symbol = context.getSymbolAtLocation(node.qualifier || node);
// #2792, we should always have a symbol here unless there is a compiler
// error ignored with ts-expect-error or ts-ignore.
if (!symbol) {
return new IntrinsicType("any");
}
return ReferenceType.createSymbolReference(context.resolveAliasedSymbol(symbol), context, name);
},
convertType(context, type) {
const symbol = type.getSymbol();
assert(symbol, "Missing symbol when converting import type"); // Should be a compiler error
return ReferenceType.createSymbolReference(context.resolveAliasedSymbol(symbol), context, "__module");
},
};
const indexedAccessConverter = {
kind: [ts.SyntaxKind.IndexedAccessType],
convert(context, node) {
return new IndexedAccessType(convertType(context, node.objectType), convertType(context, node.indexType));
},
convertType(context, type) {
return new IndexedAccessType(convertType(context, type.objectType), convertType(context, type.indexType));
},
};
const inferredConverter = {
kind: [ts.SyntaxKind.InferType],
convert(context, node) {
return new InferredType(node.typeParameter.name.text, maybeConvertType(context, node.typeParameter.constraint));
},
convertType(context, type) {
return new InferredType(type.getSymbol().name, maybeConvertType(context, type.getConstraint()));
},
};
const intersectionConverter = {
kind: [ts.SyntaxKind.IntersectionType],
convert(context, node) {
return new IntersectionType(node.types.map((type) => convertType(context, type)));
},
convertType(context, type) {
return new IntersectionType(type.types.map((type) => convertType(context, type)));
},
};
const intrinsicConverter = {
kind: [ts.SyntaxKind.IntrinsicKeyword],
convert() {
return new IntrinsicType("intrinsic");
},
convertType() {
return new IntrinsicType("intrinsic");
},
};
const jsDocVariadicTypeConverter = {
kind: [ts.SyntaxKind.JSDocVariadicType],
convert(context, node) {
return new ArrayType(convertType(context, node.type));
},
// Should just be an ArrayType
convertType: requestBugReport,
};
const keywordNames = {
[ts.SyntaxKind.AnyKeyword]: "any",
[ts.SyntaxKind.BigIntKeyword]: "bigint",
[ts.SyntaxKind.BooleanKeyword]: "boolean",
[ts.SyntaxKind.NeverKeyword]: "never",
[ts.SyntaxKind.NumberKeyword]: "number",
[ts.SyntaxKind.ObjectKeyword]: "object",
[ts.SyntaxKind.StringKeyword]: "string",
[ts.SyntaxKind.SymbolKeyword]: "symbol",
[ts.SyntaxKind.UndefinedKeyword]: "undefined",
[ts.SyntaxKind.UnknownKeyword]: "unknown",
[ts.SyntaxKind.VoidKeyword]: "void",
[ts.SyntaxKind.IntrinsicKeyword]: "intrinsic",
};
const keywordConverter = {
kind: [
ts.SyntaxKind.AnyKeyword,
ts.SyntaxKind.BigIntKeyword,
ts.SyntaxKind.BooleanKeyword,
ts.SyntaxKind.NeverKeyword,
ts.SyntaxKind.NumberKeyword,
ts.SyntaxKind.ObjectKeyword,
ts.SyntaxKind.StringKeyword,
ts.SyntaxKind.SymbolKeyword,
ts.SyntaxKind.UndefinedKeyword,
ts.SyntaxKind.UnknownKeyword,
ts.SyntaxKind.VoidKeyword,
],
convert(_context, node) {
return new IntrinsicType(keywordNames[node.kind]);
},
convertType(_context, _type, node) {
return new IntrinsicType(keywordNames[node.kind]);
},
};
const optionalConverter = {
kind: [ts.SyntaxKind.OptionalType],
convert(context, node) {
return new OptionalType(removeUndefined(convertType(context, node.type)));
},
// Handled by the tuple converter
convertType: requestBugReport,
};
const parensConverter = {
kind: [ts.SyntaxKind.ParenthesizedType],
convert(context, node) {
return convertType(context, node.type);
},
// TS strips these out too... shouldn't run into this.
convertType: requestBugReport,
};
const predicateConverter = {
kind: [ts.SyntaxKind.TypePredicate],
convert(context, node) {
const name = ts.isThisTypeNode(node.parameterName)
? "this"
: node.parameterName.getText();
const asserts = !!node.assertsModifier;
const targetType = node.type ? convertType(context, node.type) : void 0;
return new PredicateType(name, asserts, targetType);
},
// Never inferred by TS 4.0, could potentially change in a future TS version.
convertType: requestBugReport,
};
// This is a horrible thing... we're going to want to split this into converters
// for different types at some point.
const typeLiteralConverter = {
kind: [ts.SyntaxKind.TypeLiteral],
convert(context, node) {
const symbol = context.getSymbolAtLocation(node) ?? node.symbol;
const type = context.getTypeAtLocation(node);
if (!symbol || !type) {
return new IntrinsicType("Object");
}
const reflection = new DeclarationReflection("__type", ReflectionKind.TypeLiteral, context.scope);
const rc = context.withScope(reflection);
rc.convertingTypeNode = true;
context.registerReflection(reflection, symbol);
context.converter.trigger(ConverterEvents.CREATE_DECLARATION, context, reflection);
for (const prop of context.checker.getPropertiesOfType(type)) {
convertSymbol(rc, prop);
}
for (const signature of type.getCallSignatures()) {
createSignature(rc, ReflectionKind.CallSignature, signature, symbol);
}
for (const signature of type.getConstructSignatures()) {
createSignature(rc, ReflectionKind.ConstructorSignature, signature, symbol);
}
convertIndexSignatures(rc, type);
return new ReflectionType(reflection);
},
convertType(context, type) {
const symbol = type.getSymbol();
const reflection = new DeclarationReflection("__type", ReflectionKind.TypeLiteral, context.scope);
context.registerReflection(reflection, symbol);
context.converter.trigger(ConverterEvents.CREATE_DECLARATION, context, reflection);
for (const prop of context.checker.getPropertiesOfType(type)) {
convertSymbol(context.withScope(reflection), prop);
}
for (const signature of type.getCallSignatures()) {
createSignature(context.withScope(reflection), ReflectionKind.CallSignature, signature, symbol);
}
for (const signature of type.getConstructSignatures()) {
createSignature(context.withScope(reflection), ReflectionKind.ConstructorSignature, signature, symbol);
}
if (symbol) {
convertIndexSignatures(context.withScope(reflection), type);
}
return new ReflectionType(reflection);
},
};
const queryConverter = {
kind: [ts.SyntaxKind.TypeQuery],
convert(context, node) {
const querySymbol = context.getSymbolAtLocation(node.exprName);
if (!querySymbol) {
// This can happen if someone uses `typeof` on some property
// on a variable typed as `any` with a name that doesn't exist.
return new QueryType(ReferenceType.createBrokenReference(node.exprName.getText(), context.project));
}
const ref = ReferenceType.createSymbolReference(context.resolveAliasedSymbol(querySymbol), context, node.exprName.getText());
ref.preferValues = true;
return new QueryType(ref);
},
convertType(context, type, node) {
// Order matters here - check the node location first so that if the typeof is targeting
// an instantiation expression we get the user's exprName.
const symbol = context.getSymbolAtLocation(node.exprName) || type.getSymbol();
assert(symbol, `Query type failed to get a symbol for: ${context.checker.typeToString(type)}. This is a bug.`);
const ref = ReferenceType.createSymbolReference(context.resolveAliasedSymbol(symbol), context);
ref.preferValues = true;
return new QueryType(ref);
},
};
const referenceConverter = {
kind: [ts.SyntaxKind.TypeReference],
convert(context, node) {
const type = context.checker.getTypeAtLocation(node.typeName);
const isArray = context.checker.typeToTypeNode(type, void 0, ts.NodeBuilderFlags.IgnoreErrors)?.kind === ts.SyntaxKind.ArrayType;
if (isArray) {
return new ArrayType(convertType(context, node.typeArguments?.[0]));
}
const symbol = context.expectSymbolAtLocation(node.typeName);
// Ignore @inline if there are type arguments, as they won't be resolved
// in the type we just retrieved from node.typeName.
if (!node.typeArguments &&
context
.getComment(symbol, ReflectionKind.Interface)
?.hasModifier("@inline")) {
// typeLiteralConverter doesn't use the node, so we can get away with lying here.
// This might not actually be safe, it appears that it is in the relatively small
// amount of testing I've done with it, but I wouldn't be surprised if someone manages
// to find a crash.
return typeLiteralConverter.convertType(context, type, null);
}
const name = node.typeName.getText();
const ref = ReferenceType.createSymbolReference(context.resolveAliasedSymbol(symbol), context, name);
ref.typeArguments = node.typeArguments?.map((type) => convertType(context, type));
return ref;
},
convertType(context, type, node) {
// typeName.symbol handles the case where this is a union which happens to refer
// to an enumeration. TS doesn't put the symbol on the type for some reason, but
// does add it to the constructed type node.
const symbol = type.aliasSymbol ?? type.getSymbol() ?? node.typeName.symbol;
if (!symbol) {
// This happens when we get a reference to a type parameter
// created within a mapped type, `K` in: `{ [K in T]: string }`
const ref = ReferenceType.createBrokenReference(context.checker.typeToString(type), context.project);
ref.refersToTypeParameter = true;
return ref;
}
if (context
.getComment(symbol, ReflectionKind.Interface)
?.hasModifier("@inline")) {
// typeLiteralConverter doesn't use the node, so we can get away with lying here.
// This might not actually be safe, it appears that it is in the relatively small
// amount of testing I've done with it, but I wouldn't be surprised if someone manages
// to find a crash.
return typeLiteralConverter.convertType(context, type, null);
}
let name;
if (ts.isIdentifier(node.typeName)) {
name = node.typeName.text;
}
else {
name = node.typeName.right.text;
}
const ref = ReferenceType.createSymbolReference(context.resolveAliasedSymbol(symbol), context, name);
if (type.flags & ts.TypeFlags.Substitution) {
// NoInfer<T>
ref.typeArguments = [
convertType(context, type.baseType),
];
}
else if (type.flags & ts.TypeFlags.StringMapping) {
ref.typeArguments = [
convertType(context, type.type),
];
}
else {
// Default type arguments are filled with a reference to the default
// type. As TS doesn't support specifying earlier defaults, we know
// that this will only filter out type arguments which aren't specified
// by the user.
let ignoredArgs;
if (isTypeReference(type)) {
ignoredArgs = type.target.typeParameters
?.map((p) => p.getDefault())
.filter((x) => !!x);
}
const args = type.aliasSymbol
? type.aliasTypeArguments
: type.typeArguments;
ref.typeArguments = args
?.filter((ref) => !ignoredArgs?.includes(ref))
.map((ref) => convertType(context, ref));
}
return ref;
},
};
const restConverter = {
kind: [ts.SyntaxKind.RestType],
convert(context, node) {
return new RestType(convertType(context, node.type));
},
// This is handled in the tuple converter
convertType: requestBugReport,
};
const namedTupleMemberConverter = {
kind: [ts.SyntaxKind.NamedTupleMember],
convert(context, node) {
const innerType = convertType(context, node.type);
return new NamedTupleMember(node.name.getText(), !!node.questionToken, innerType);
},
// This ought to be impossible.
convertType: requestBugReport,
};
// { -readonly [K in string]-?: number}
// ^ readonlyToken
// ^ typeParameter
// ^^^^^^ typeParameter.constraint
// ^ questionToken
// ^^^^^^ type
const mappedConverter = {
kind: [ts.SyntaxKind.MappedType],
convert(context, node) {
const optionalModifier = kindToModifier(node.questionToken?.kind);
const templateType = convertType(context, node.type);
return new MappedType(node.typeParameter.name.text, convertType(context, node.typeParameter.constraint), optionalModifier === "+"
? removeUndefined(templateType)
: templateType, kindToModifier(node.readonlyToken?.kind), optionalModifier, node.nameType ? convertType(context, node.nameType) : void 0);
},
convertType(context, type, node) {
// This can happen if a generic function does not have a return type annotated.
const optionalModifier = kindToModifier(node.questionToken?.kind);
const templateType = convertType(context, type.templateType);
return new MappedType(type.typeParameter.symbol.name || "__type", convertType(context, type.typeParameter.getConstraint()), optionalModifier === "+"
? removeUndefined(templateType)
: templateType, kindToModifier(node.readonlyToken?.kind), optionalModifier, type.nameType ? convertType(context, type.nameType) : void 0);
},
};
const literalTypeConverter = {
kind: [ts.SyntaxKind.LiteralType],
convert(context, node) {
switch (node.literal.kind) {
case ts.SyntaxKind.TrueKeyword:
case ts.SyntaxKind.FalseKeyword:
return new LiteralType(node.literal.kind === ts.SyntaxKind.TrueKeyword);
case ts.SyntaxKind.StringLiteral:
return new LiteralType(node.literal.text);
case ts.SyntaxKind.NumericLiteral:
return new LiteralType(Number(node.literal.text));
case ts.SyntaxKind.NullKeyword:
return new LiteralType(null);
case ts.SyntaxKind.PrefixUnaryExpression: {
const operand = node.literal
.operand;
switch (operand.kind) {
case ts.SyntaxKind.NumericLiteral:
return new LiteralType(Number(node.literal.getText()));
case ts.SyntaxKind.BigIntLiteral:
return new LiteralType(BigInt(node.literal.getText().replace("n", "")));
default:
return requestBugReport(context, node.literal);
}
}
case ts.SyntaxKind.BigIntLiteral:
return new LiteralType(BigInt(node.literal.getText().replace("n", "")));
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
return new LiteralType(node.literal.text);
}
return requestBugReport(context, node.literal);
},
convertType(_context, type, node) {
switch (node.literal.kind) {
case ts.SyntaxKind.StringLiteral:
return new LiteralType(node.literal.text);
case ts.SyntaxKind.NumericLiteral:
return new LiteralType(+node.literal.text);
case ts.SyntaxKind.TrueKeyword:
case ts.SyntaxKind.FalseKeyword:
return new LiteralType(node.literal.kind === ts.SyntaxKind.TrueKeyword);
case ts.SyntaxKind.NullKeyword:
return new LiteralType(null);
}
if (typeof type.value === "object") {
return new LiteralType(BigInt(`${type.value.negative ? "-" : ""}${type.value.base10Value}`));
}
return new LiteralType(type.value);
},
};
const templateLiteralConverter = {
kind: [ts.SyntaxKind.TemplateLiteralType],
convert(context, node) {
return new TemplateLiteralType(node.head.text, node.templateSpans.map((span) => {
return [convertType(context, span.type), span.literal.text];
}));
},
convertType(context, type) {
assert(type.texts.length === type.types.length + 1);
const parts = [];
for (const [a, b] of zip(type.types, type.texts.slice(1))) {
parts.push([convertType(context, a), b]);
}
return new TemplateLiteralType(type.texts[0], parts);
},
};
const thisConverter = {
kind: [ts.SyntaxKind.ThisType],
convert() {
return new IntrinsicType("this");
},
convertType() {
return new IntrinsicType("this");
},
};
const tupleConverter = {
kind: [ts.SyntaxKind.TupleType],
convert(context, node) {
const elements = node.elements.map((node) => convertType(context, node));
return new TupleType(elements);
},
convertType(context, type, node) {
const types = type.typeArguments?.slice(0, node.elements.length);
let elements = types?.map((type) => convertType(context, type));
if (type.target.labeledElementDeclarations) {
const namedDeclarations = type.target.labeledElementDeclarations;
elements = elements?.map((el, i) => {
const namedDecl = namedDeclarations[i];
return namedDecl
? new NamedTupleMember(namedDecl.name.getText(), !!namedDecl.questionToken, removeUndefined(el))
: el;
});
}
elements = elements?.map((el, i) => {
if (type.target.elementFlags[i] & ts.ElementFlags.Variable) {
// In the node case, we don't need to add the wrapping Array type... but we do here.
if (el instanceof NamedTupleMember) {
return new RestType(new NamedTupleMember(el.name, el.isOptional, new ArrayType(el.element)));
}
return new RestType(new ArrayType(el));
}
if (type.target.elementFlags[i] & ts.ElementFlags.Optional &&
!(el instanceof NamedTupleMember)) {
return new OptionalType(removeUndefined(el));
}
return el;
});
return new TupleType(elements ?? []);
},
};
const supportedOperatorNames = {
[ts.SyntaxKind.KeyOfKeyword]: "keyof",
[ts.SyntaxKind.UniqueKeyword]: "unique",
[ts.SyntaxKind.ReadonlyKeyword]: "readonly",
};
const typeOperatorConverter = {
kind: [ts.SyntaxKind.TypeOperator],
convert(context, node) {
return new TypeOperatorType(convertType(context, node.type), supportedOperatorNames[node.operator]);
},
convertType(context, type, node) {
// readonly is only valid on array and tuple literal types.
if (node.operator === ts.SyntaxKind.ReadonlyKeyword) {
const resolved = resolveReference(type);
assert(isObjectType(resolved));
const args = context.checker
.getTypeArguments(type)
.map((type) => convertType(context, type));
const inner = resolved.objectFlags & ts.ObjectFlags.Tuple
? new TupleType(args)
: new ArrayType(args[0]);
return new TypeOperatorType(inner, "readonly");
}
// keyof will only show up with generic functions, otherwise it gets eagerly
// resolved to a union of strings.
if (node.operator === ts.SyntaxKind.KeyOfKeyword) {
// There's probably an interface for this somewhere... I couldn't find it.
const targetType = type.type;
return new TypeOperatorType(convertType(context, targetType), "keyof");
}
// TS drops `unique` in `unique symbol` everywhere. If someone used it, we ought
// to have a type node. This shouldn't ever happen.
return requestBugReport(context, type);
},
};
const unionConverter = {
kind: [ts.SyntaxKind.UnionType],
convert(context, node) {
return new UnionType(node.types.map((type) => convertType(context, type)));
},
convertType(context, type) {
const types = type.types.map((type) => convertType(context, type));
normalizeUnion(types);
sortLiteralUnion(types);
return new UnionType(types);
},
};
const jsDocNullableTypeConverter = {
kind: [ts.SyntaxKind.JSDocNullableType],
convert(context, node) {
return new UnionType([
convertType(context, node.type),
new LiteralType(null),
]);
},
// Should be a UnionType
convertType: requestBugReport,
};
const jsDocNonNullableTypeConverter = {
kind: [ts.SyntaxKind.JSDocNonNullableType],
convert(context, node) {
return convertType(context, node.type);
},
// Should be a UnionType
convertType: requestBugReport,
};
function requestBugReport(context, nodeOrType) {
if ("kind" in nodeOrType) {
const kindName = ts.SyntaxKind[nodeOrType.kind];
context.logger.warn(`Failed to convert type node with kind: ${kindName} and text ${nodeOrType.getText()}. Please report a bug.`, nodeOrType);
return new UnknownType(nodeOrType.getText());
}
else {
const typeString = context.checker.typeToString(nodeOrType);
context.logger.warn(`Failed to convert type: ${typeString} when converting ${context.scope.getFullName()}. Please report a bug.`);
return new UnknownType(typeString);
}
}
function resolveReference(type) {
if (isObjectType(type) && type.objectFlags & ts.ObjectFlags.Reference) {
return type.target;
}
return type;
}
function kindToModifier(kind) {
switch (kind) {
case ts.SyntaxKind.ReadonlyKeyword:
case ts.SyntaxKind.QuestionToken:
case ts.SyntaxKind.PlusToken:
return "+";
case ts.SyntaxKind.MinusToken:
return "-";
default:
return undefined;
}
}
function sortLiteralUnion(types) {
if (types.some((t) => t.type !== "literal" || typeof t.value !== "number")) {
return;
}
types.sort((a, b) => {
const aLit = a;
const bLit = b;
return aLit.value - bLit.value;
});
}
function normalizeUnion(types) {
let trueIndex = -1;
let falseIndex = -1;
for (let i = 0; i < types.length && (trueIndex === -1 || falseIndex === -1); i++) {
const t = types[i];
if (t instanceof LiteralType) {
if (t.value === true) {
trueIndex = i;
}
if (t.value === false) {
falseIndex = i;
}
}
}
if (trueIndex !== -1 && falseIndex !== -1) {
types.splice(Math.max(trueIndex, falseIndex), 1);
types.splice(Math.min(trueIndex, falseIndex), 1, new IntrinsicType("boolean"));
}
}