UNPKG

type-to-string

Version:

Get a string representation of a TypeScript type.

238 lines (233 loc) 8.68 kB
import ts from 'typescript'; import { isIntrinsicType, isIntrinsicErrorType } from 'ts-api-utils'; /** * Merge the given type and type arguments string representations. */ function getTypeWithTypeArgumentsString(name, typeArguments) { if (typeArguments === undefined) { return name; } if (typeArguments.includes(null)) { return null; } return `${name}<${typeArguments.join(", ")}>`; } /** * Get string representations of the given entity name. */ function entityNameToString( // eslint-disable-next-line functional/prefer-immutable-types entityName) { return ts.isIdentifier(entityName) ? identifierToString(entityName) : qualifiedNameToString(entityName); } /** * Get string representations of the given identifier. */ function identifierToString( // eslint-disable-next-line functional/prefer-immutable-types identifier) { return identifier.escapedText; } /** * Get string representations of the given qualified name. */ // eslint-disable-next-line functional/prefer-immutable-types function qualifiedNameToString(qualifiedName) { return `${entityNameToString(qualifiedName.left)}.${identifierToString(qualifiedName.right)}`; } /** * Get the highest set bit in the given value. */ function bitwiseMax(value) { let m_shifts = 0; // eslint-disable-next-line functional/no-loop-statements, no-cond-assign, no-param-reassign while ((value >>= 1) !== 0) { // eslint-disable-next-line functional/no-expression-statements m_shifts++; } return 1 << m_shifts; } const typeFormatFlagsMaxValue = bitwiseMax(Object.values(ts.TypeFormatFlags).reduce((carry, current) => typeof current === "number" ? carry | current : carry, 0)); /** * The same as {@link TypeFormatFlags} but with a few extra options added on. */ const TypeNodeFormatFlags = { ...ts.TypeFormatFlags, OmitTypeLiterals: typeFormatFlagsMaxValue << 1, }; /** * Get the alias name of the given {@link TypeNode}. */ function getTypeNodeAliasAsString( // eslint-disable-next-line functional/prefer-immutable-types typeNode, withArguments = false) { if (ts.isTypeAliasDeclaration(typeNode.parent)) { const name = identifierToString(typeNode.parent.name); if (!withArguments) { return name; } const typeArguments = typeNode.parent.typeParameters?.map((type) => identifierToString(type.name)); return getTypeWithTypeArgumentsString(name, typeArguments); } return null; } /** * Get the name of the given {@link TypeReferenceNode}. * * If the given {@link Node} is not a {@link TypeReferenceNode}, `null` will be returned. */ function getTypeReferenceNodeAsString(program, node, withArguments = false) { if (ts.isTypeReferenceNode(node)) { const name = entityNameToString(node.typeName); if (!withArguments) { return name; } const checker = program.getTypeChecker(); const type = checker.getTypeAtLocation(node.typeName); type.aliasSymbol ?? type.getSymbol(); const typeArgumentsNodes = type.aliasTypeArguments ?? (type.typeParameters); const typeArguments = typeArgumentsNodes?.map((parameter) => parameter.getSymbol()?.getName() ?? null); return getTypeWithTypeArgumentsString(name, typeArguments); } if (ts.isArrayTypeNode(node)) { return withArguments ? "Array<T>" : "Array"; } if (ts.isTypeOperatorNode(node) && node.operator === ts.SyntaxKind.ReadonlyKeyword && ts.isArrayTypeNode(node.type)) { return withArguments ? "ReadonlyArray<T>" : "ReadonlyArray"; } return null; } /** * Get the {@link TypeNode} as written. */ function typeNodeAsWritten( // eslint-disable-next-line functional/prefer-immutable-types typeNode, keepAllWhiteSpace = false) { if (keepAllWhiteSpace) { return typeNode.getFullText(); } return typeNode.getText(); } /** * Get string representations of the given {@link TypeNode}. * * @throws When the given type node is anonymous. */ function typeNodeToString(program, // eslint-disable-next-line functional/prefer-immutable-types typeNode, flags = ts.TypeFormatFlags.None) { return getTypeNodeString(program, typeNode, flags); } function getTypeNodeString(program, // eslint-disable-next-line functional/prefer-immutable-types typeNode, flags, readonly = false) { if (ts.isTypeOperatorNode(typeNode)) { return getTypeNodeString(program, typeNode.type, flags, typeNode.operator === ts.SyntaxKind.ReadonlyKeyword); } if (ts.isTypeReferenceNode(typeNode)) { return typeReferenceNodeToString(program, typeNode, flags); } if (ts.isArrayTypeNode(typeNode)) { return arrayTypeNodeToString(program, typeNode, flags, readonly); } if (ts.isTupleTypeNode(typeNode)) { return tupleTypeNodeToString(program, typeNode, flags, readonly); } if (ts.isTypeLiteralNode(typeNode)) { return typeLiteralNodeToString(program, typeNode, flags); } if (ts.isTypeQueryNode(typeNode)) { return typeQueryNodeToString(program, typeNode, flags); } const checker = program.getTypeChecker(); const type = checker.getTypeAtLocation(typeNode); if (isIntrinsicType(type)) { return type.intrinsicName; } // TODO: implement return null; } function typeReferenceNodeToString(program, // eslint-disable-next-line functional/prefer-immutable-types typeNode, flags) { const name = entityNameToString(typeNode.typeName); const typeArguments = typeNode.typeArguments?.map((type) => getTypeNodeString(program, type, flags)); return getTypeWithTypeArgumentsString(name, typeArguments); } function typeLiteralNodeToString(program, // eslint-disable-next-line functional/prefer-immutable-types typeNode, flags) { if ((flags & TypeNodeFormatFlags.OmitTypeLiterals) !== 0) { return "{}"; } // TODO: implement return "{}"; } function arrayTypeNodeToString(program, // eslint-disable-next-line functional/prefer-immutable-types typeNode, flags, readonly) { const typeArgument = getTypeNodeString(program, typeNode.elementType, flags); if ((flags & TypeNodeFormatFlags.WriteArrayAsGenericType) !== 0) { const name = readonly ? "ReadonlyArray" : "Array"; return getTypeWithTypeArgumentsString(name, [typeArgument]); } return `${readonly ? "readonly " : ""}${typeArgument}[]`; } function tupleTypeNodeToString(program, // eslint-disable-next-line functional/prefer-immutable-types typeNode, flags, readonly) { const typeArguments = typeNode.elements.map((type) => getTypeNodeString(program, ts.isNamedTupleMember(type) ? type.type : type, flags)); return `${readonly ? "readonly " : ""}[${typeArguments.join(", ")}]`; } function typeQueryNodeToString(program, // eslint-disable-next-line functional/prefer-immutable-types typeNode, flags) { const checker = program.getTypeChecker(); const type = checker.getTypeAtLocation(typeNode.exprName); if (isIntrinsicErrorType(type)) { return null; } return typeToString(program, type, typeNode.exprName, flags); } /** * Get string representations of the given {@link Type}. */ function typeToString(program, // eslint-disable-next-line functional/prefer-immutable-types type, enclosingDeclaration = undefined, flags = ts.TypeFormatFlags.None) { const checker = program.getTypeChecker(); return checker.typeToString(type, enclosingDeclaration, flags); } /** * Get the alias name of the given {@link Type}. */ function getTypeAliasAsString( // eslint-disable-next-line functional/prefer-immutable-types type, withArguments = false) { const t = "target" in type ? type.target : type; const name = t.aliasSymbol?.getName() ?? null; if (!withArguments || name === null) { return name; } const typeArguments = t.aliasTypeArguments?.map((argument) => argument.getSymbol()?.getName() ?? null); return getTypeWithTypeArgumentsString(name, typeArguments); } /** * Get the name of the type reference on the given {@link Type}. * * If the given {@link Type} is not a type reference, `null` will be returned. */ function getTypeReferenceAsString(program, // eslint-disable-next-line functional/prefer-immutable-types type, withArguments = false) { if (!("node" in type)) { return null; } return getTypeReferenceNodeAsString(program, type.node, withArguments); } export { TypeNodeFormatFlags, getTypeAliasAsString, getTypeNodeAliasAsString, getTypeReferenceAsString, getTypeReferenceNodeAsString, typeNodeAsWritten, typeNodeToString, typeToString };