UNPKG

typedoc

Version:

Create api documentation for TypeScript projects.

789 lines (788 loc) 34.7 kB
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")); } }