@mui/internal-docs-infra
Version:
MUI Infra - internal documentation creation tools.
752 lines (711 loc) • 31.6 kB
JavaScript
import { uniq } from 'es-toolkit';
import { isExternalType, isIntrinsicType, isUnionType, isIntersectionType, isObjectType, isAnonymousObjectType, isArrayType, isFunctionType, isLiteralType, isTupleType, isTypeParameterType, isInternalTypeName } from "./typeGuards.mjs";
import { isOwnTypeName, maybeCollectExternalUnion, maybeCollectExternalFunction, maybeCollectExternalReference } from "./externalTypes.mjs";
import { prettyFormat } from "./format.mjs";
export function formatType(type, options) {
const {
removeUndefined = false,
jsdocTags,
expandObjects = false,
exportNames,
typeNameMap,
externalTypesCollector,
selfName,
withPropertyComments,
preserveTypeParameters
} = options;
/**
* Checks if a qualified type name matches the selfName (type being defined).
* Strips type arguments before comparing to handle generic instances.
* e.g., "Tabs.Root.ChangeEventDetails<'none', ...>" should match "Tabs.Root.ChangeEventDetails"
*/
function matchesSelfName(qualifiedName, simpleName) {
if (!selfName) {
return false;
}
// Strip type arguments from the qualified name for comparison
const baseQualifiedName = qualifiedName.replace(/<.*>$/, '');
return simpleName === selfName || baseQualifiedName === selfName ||
// Also check dots-removed form to handle cases where selfName is the
// flattened display name (e.g., "FormState") and qualifiedName is dotted
// (e.g., "Form.State")
baseQualifiedName.replace(/\./g, '') === selfName;
}
const typeTag = jsdocTags?.find?.(tag => tag.name === 'type');
const typeValue = typeTag?.value;
if (typeValue) {
return typeValue;
}
if (isExternalType(type)) {
if (/^ReactElement(<.*>)?/.test(type.typeName.name || '')) {
return 'ReactElement';
}
if (type.typeName.namespaces?.length === 1 && type.typeName.namespaces[0] === 'React') {
return createNameWithTypeArguments(type.typeName, exportNames, typeNameMap, externalTypesCollector, preserveTypeParameters);
}
const qualifiedName = getFullyQualifiedName(type.typeName, exportNames, typeNameMap, preserveTypeParameters);
if (externalTypesCollector) {
// Only collect as external if the qualified name wasn't rewritten to an own type.
if (qualifiedName === type.typeName.name || !isOwnTypeName(qualifiedName, externalTypesCollector)) {
maybeCollectExternalReference(type, externalTypesCollector);
}
}
return qualifiedName;
}
if (isIntrinsicType(type)) {
return type.typeName ? getFullyQualifiedName(type.typeName, exportNames, typeNameMap, preserveTypeParameters) : type.intrinsic;
}
if (isUnionType(type)) {
// For union types with a type alias name, always prefer showing the alias name
// (e.g., 'StoreAtMode' instead of expanding to "'canonical' | 'import' | 'flat'")
// The expandObjects flag is primarily for object types where showing the structure is valuable
// But skip if the type name matches selfName to avoid circular references like `type Foo = Foo`
if (type.typeName) {
const qualifiedName = getFullyQualifiedName(type.typeName, exportNames, typeNameMap, preserveTypeParameters);
// Check both the simple name AND the fully qualified name against selfName
// selfName can be either format depending on context
// Also strip type arguments to catch cases like `type Foo = Foo<Args>`
if (!matchesSelfName(qualifiedName, type.typeName.name)) {
if (externalTypesCollector) {
// Only collect as external if the qualified name wasn't rewritten to an own type.
// e.g., BaseOrientation → Tabs.Root.Orientation means it's an own type alias,
// not an external type that should appear in the External Types section.
if (qualifiedName === type.typeName.name || !isOwnTypeName(qualifiedName, externalTypesCollector)) {
maybeCollectExternalUnion(type, externalTypesCollector);
}
}
return qualifiedName;
}
}
let memberTypes = type.types;
if (removeUndefined) {
memberTypes = memberTypes.filter(t => !(isIntrinsicType(t) && t.intrinsic === 'undefined'));
}
// Deduplicates types in unions.
// Plain unions are handled by TypeScript API Extractor, but we also display unions in type parameters constraints,
// so we need to merge those here.
const flattenedMemberTypes = memberTypes.flatMap(t => {
if (isUnionType(t)) {
return t.typeName ? t : t.types;
}
if (isTypeParameterType(t) && isUnionType(t.constraint)) {
return t.constraint.types;
}
return t;
});
// When all union members are object types, try to extract common properties
// into an intersection to reduce repetition.
// e.g., { reason: 'a'; shared: X } | { reason: 'b'; shared: X }
// → ({ reason: 'a' } | { reason: 'b' }) & { shared: X }
const merged = mergeObjectUnionCommonProperties(flattenedMemberTypes, {
removeUndefined,
exportNames,
typeNameMap,
externalTypesCollector,
withPropertyComments,
preserveTypeParameters
});
if (merged) {
return merged;
}
const formattedMemeberTypes = uniq(orderMembers(flattenedMemberTypes).map(t =>
// Use expandObjects=false for nested types to prevent deep expansion
formatType(t, {
removeUndefined,
exportNames,
typeNameMap,
externalTypesCollector,
preserveTypeParameters
})));
return formattedMemeberTypes.join(' | ');
}
if (isIntersectionType(type)) {
// For intersection types with a type alias name, always prefer showing the alias name
// The expandObjects flag is primarily for object types where showing the structure is valuable
// But skip if the type name matches selfName to avoid circular references like `type Foo = Foo`
if (type.typeName) {
const qualifiedName = getFullyQualifiedName(type.typeName, exportNames, typeNameMap, preserveTypeParameters);
// Check both the simple name AND the fully qualified name against selfName
// selfName can be either format depending on context
// Also strip type arguments to catch cases like `type Foo = Foo<Args>`
if (!matchesSelfName(qualifiedName, type.typeName.name)) {
return qualifiedName;
}
}
// Check if all members are object types - if so, merge them into a single object
const allAreObjects = type.types.every(t => isObjectType(t));
if (allAreObjects) {
// Merge all properties from all object types. When the same property
// name appears in more than one member of the intersection (e.g. when an
// inline object literal re-states a field already present on a named
// interface), keep the last occurrence so the rendered output stays a
// valid object literal instead of containing duplicate keys.
const propertiesByName = new Map();
for (const t of type.types) {
if (!isObjectType(t)) {
continue;
}
for (const property of t.properties ?? []) {
propertiesByName.set(property.name, property);
}
}
const mergedProperties = Array.from(propertiesByName.values());
if (mergedProperties.length > 0) {
const parts = mergedProperties.map(m => {
const propertyName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(m.name) ? m.name : `'${m.name}'`;
const typeStr = formatType(m.type, {
removeUndefined: m.optional,
exportNames,
typeNameMap,
externalTypesCollector,
withPropertyComments,
preserveTypeParameters
});
const propLine = `${propertyName}${m.optional ? '?' : ''}: ${typeStr}`;
if (withPropertyComments && m.documentation) {
const comment = formatPropertyComment(m.documentation);
if (comment) {
return `${comment}\n${propLine}`;
}
}
return propLine;
});
const hasComments = withPropertyComments && parts.some(p => p.includes('/**'));
const separator = hasComments ? ';\n' : '; ';
return `{ ${parts.join(separator)} }`;
}
}
const formattedMembers = orderMembers(type.types)
// Use expandObjects=false for nested types to prevent deep expansion
.map(t => formatType(t, {
exportNames,
typeNameMap,
externalTypesCollector,
preserveTypeParameters
}))
// Filter out empty objects (e.g., `& {}` from generic defaults)
.filter(formatted => formatted !== '{}' && formatted !== '{ }');
// If all members were filtered out, return empty object
if (formattedMembers.length === 0) {
return '{}';
}
// If only one member remains, return it without intersection
if (formattedMembers.length === 1) {
return formattedMembers[0];
}
return formattedMembers.join(' & ');
}
if (isObjectType(type)) {
// Check if the object has an index signature
const indexSignature = type.indexSignature;
// Check if the type name is a TypeScript internal symbol name (e.g., __object, __type)
// These are anonymous types and should not be displayed as-is
const hasValidTypeName = type.typeName && !isInternalTypeName(type.typeName.name);
// If the type has a name and we're not expanding objects, return the type name
// BUT if the type name matches selfName, we need to expand to avoid circular references
// like `type ToastManager = ToastManager`
if (hasValidTypeName && !expandObjects) {
const qualifiedName = getFullyQualifiedName(type.typeName, exportNames, typeNameMap, preserveTypeParameters);
if (!matchesSelfName(qualifiedName, type.typeName.name)) {
return qualifiedName;
}
// Fall through to expand the type since it's a self-reference
}
// If the object is empty (no properties or index signature), use the type name if available
// This ensures types like `DialogHandle<Payload>` are shown instead of `{}`
if (isObjectEmpty(type.properties) && !indexSignature) {
if (hasValidTypeName) {
const qualifiedName = getFullyQualifiedName(type.typeName, exportNames, typeNameMap, preserveTypeParameters);
if (!matchesSelfName(qualifiedName, type.typeName.name)) {
return qualifiedName;
}
}
return '{}';
}
const parts = [];
// Add index signature if present
// Use expandObjects=false for value types to prevent deep expansion (one level only)
if (indexSignature) {
const valueTypeStr = formatType(indexSignature.valueType, {
exportNames,
typeNameMap,
externalTypesCollector,
preserveTypeParameters
});
// Use the original key name if available, otherwise fall back to 'key'
const keyName = indexSignature.keyName || 'key';
parts.push(`[${keyName}: ${indexSignature.keyType}]: ${valueTypeStr}`);
}
// Add regular properties
// Use expandObjects=false for property types to prevent deep expansion (one level only)
// Dedupe by name first: the api-extractor can emit the same property twice
// when an inline object literal re-declares a field already present on a
// named interface (e.g. `ParsedCreateFactory & { foo?: T }` where `foo` is
// already on `ParsedCreateFactory`). Keep the last occurrence so the
// rendered object literal stays valid.
const propertiesByName = new Map();
for (const property of type.properties) {
propertiesByName.set(property.name, property);
}
parts.push(...Array.from(propertiesByName.values()).map(m => {
// Property names with hyphens or other special characters need quotes
const propertyName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(m.name) ? m.name : `'${m.name}'`;
const typeStr = formatType(m.type, {
removeUndefined: m.optional,
exportNames,
typeNameMap,
externalTypesCollector,
withPropertyComments,
preserveTypeParameters
});
const propLine = `${propertyName}${m.optional ? '?' : ''}: ${typeStr}`;
if (withPropertyComments && m.documentation) {
const comment = formatPropertyComment(m.documentation);
if (comment) {
return `${comment}\n${propLine}`;
}
}
return propLine;
}));
// Use newline separators when comments are present so Prettier
// places each comment on its own line above its property
const hasComments = withPropertyComments && parts.some(p => p.includes('/**'));
const separator = hasComments ? ';\n' : '; ';
return `{ ${parts.join(separator)} }`;
}
if (isLiteralType(type)) {
return normalizeQuotes(String(type.value));
}
if (isArrayType(type)) {
// Use expandObjects=false for element types to prevent deep expansion (one level only)
const formattedMemberType = formatType(type.elementType, {
exportNames,
typeNameMap,
externalTypesCollector,
preserveTypeParameters
});
if (formattedMemberType.includes(' ')) {
return `(${formattedMemberType})[]`;
}
return `${formattedMemberType}[]`;
}
if (isFunctionType(type)) {
// If a function type has a typeName, it's a named type alias (like OffsetFunction).
// Show the type name instead of expanding the full signature.
// Anonymous functions (like `() => void` inline) don't have typeNames.
//
// Exception: ComponentRenderFn types are internal implementation details and should be expanded.
// Exception: When expandObjects is true (for detailed type view), always expand.
const hasNamedTypeAlias = type.typeName?.name && !type.typeName.name.startsWith('ComponentRenderFn');
if (hasNamedTypeAlias && !expandObjects) {
const qualifiedName = getFullyQualifiedName(type.typeName, exportNames, typeNameMap, preserveTypeParameters);
if (externalTypesCollector) {
// Only collect as external if the qualified name wasn't rewritten to an own type.
if (qualifiedName === type.typeName.name || !isOwnTypeName(qualifiedName, externalTypesCollector)) {
maybeCollectExternalFunction(type, externalTypesCollector);
}
}
return qualifiedName;
}
const signatures = type.callSignatures.map(s => {
// Format method-level type parameters (e.g., `<T extends Data = Data>` on individual methods)
const genericPrefix = s.typeParameters?.length ? `<${s.typeParameters.map(tp => {
let result = tp.name;
if (tp.constraint !== undefined) {
result += ` extends ${formatType(tp.constraint, {
exportNames,
typeNameMap,
externalTypesCollector,
preserveTypeParameters
})}`;
}
if (tp.defaultValue !== undefined) {
result += ` = ${formatType(tp.defaultValue, {
exportNames,
typeNameMap,
externalTypesCollector,
preserveTypeParameters
})}`;
}
return result;
}).join(', ')}>` : '';
// Preserve method-level type parameter names within this signature's body
const localPreserve = preserveTypeParameters || (s.typeParameters?.length ?? 0) > 0;
// Use expandObjects=false for nested types to prevent deep expansion (one level only)
const params = s.parameters.map((p, index, allParams) => {
let paramType = formatType(p.type, {
exportNames,
typeNameMap,
externalTypesCollector,
preserveTypeParameters: localPreserve
});
// Check if the type includes undefined
const hasUndefined = paramType.includes('| undefined') || paramType.includes('undefined |');
// Use ?: syntax for optional parameters only if all following parameters are also optional
// This ensures we maintain valid TypeScript syntax (optional params must come last)
if (p.optional || hasUndefined) {
const remainingParams = allParams.slice(index + 1);
const allRemainingAreOptional = remainingParams.every(remaining => {
// If the parameter is explicitly marked as optional, we don't need to check the type
if (remaining.optional) {
return true;
}
// Only check the type if the parameter is not explicitly optional
// Check if it's a union with undefined without formatting the entire type
if (isUnionType(remaining.type)) {
return remaining.type.types.some(t => isIntrinsicType(t) && t.intrinsic === 'undefined');
}
return false;
});
if (allRemainingAreOptional) {
// Remove | undefined from the type since we're using ?:
paramType = paramType.replace(/\s*\|\s*undefined\s*$/, '').replace(/^\s*undefined\s*\|\s*/, '').trim();
return `${p.name}?: ${paramType}`;
}
}
return `${p.name}: ${paramType}`;
}).join(', ');
// Use expandObjects=false for return type to prevent deep expansion (one level only)
const returnType = formatType(s.returnValueType, {
exportNames,
typeNameMap,
externalTypesCollector,
preserveTypeParameters: localPreserve
});
return `${genericPrefix}(${params}) => ${returnType}`;
});
// When there are multiple signatures (overloads), each function type must be
// parenthesized before joining with | to avoid ambiguous parsing
// e.g., ((a: string) => void) | ((b: number) => void)
const functionSignature = signatures.length > 1 ? signatures.map(sig => `(${sig})`).join(' | ') : signatures.join(' | ');
return `(${functionSignature})`;
}
if (isTupleType(type)) {
if (type.typeName) {
return getFullyQualifiedName(type.typeName, exportNames, typeNameMap, preserveTypeParameters);
}
// Use expandObjects=false for tuple members to prevent deep expansion (one level only)
return `[${type.types.map(member => formatType(member, {
exportNames,
typeNameMap,
externalTypesCollector,
preserveTypeParameters
})).join(', ')}]`;
}
if (isTypeParameterType(type)) {
// When preserveTypeParameters is true, return the parameter name (e.g., "T")
// instead of expanding to its constraint. This is used by formatRaw where the
// constraint is shown in the type parameter declaration (e.g., `<T extends string>`)
// via formatTypeParameterDeclaration, so expanding here would lose the generic
// identity and produce misleading output.
// Exception: if the type parameter name matches selfName, expanding is required
// to avoid circular references like `type FormValues = FormValues;`
if (preserveTypeParameters && type.name !== selfName) {
return type.name;
}
return type.constraint !== undefined ? formatType(type.constraint, {
expandObjects,
exportNames,
typeNameMap,
externalTypesCollector
}) : type.name;
}
return 'unknown';
}
/**
* Formats a TypeScript type into a prettified string representation.
*
* This is a convenience wrapper around `formatType()` that applies Prettier formatting
* to the resulting type string. It delegates to `formatType()` for the core type
* processing, then runs the output through `prettyFormat()` for consistent styling.
*/
export async function prettyFormatType(type, options) {
return prettyFormat(formatType(type, options), type.kind === 'object' ? type.typeName?.name : undefined);
}
export function getFullyQualifiedName(typeName, exportNames, typeNameMap, preserveTypeParameters) {
const nameWithTypeArgs = createNameWithTypeArguments(typeName, exportNames, typeNameMap, undefined, preserveTypeParameters); // Note: externalTypesCollector not threaded here since getFullyQualifiedName is name-only lookup
// Construct the flat name (what parseExports would have created)
const flatName = typeName.namespaces && typeName.namespaces.length > 0 ? typeName.namespaces.join('') + typeName.name : typeName.name;
// Check if this type is in our map (exact match)
if (typeNameMap[flatName]) {
// This is one of our component types - use the mapped dotted name
const typeArgsStart = nameWithTypeArgs.indexOf('<');
if (typeArgsStart !== -1) {
// Preserve type arguments
return typeNameMap[flatName] + nameWithTypeArgs.slice(typeArgsStart);
}
return typeNameMap[flatName];
}
// Check if flatName matches a dotted export with dots removed
// e.g., ComponentPartState -> Component.Part.State (if that export exists)
for (const dottedName of Object.values(typeNameMap)) {
if (dottedName.replace(/\./g, '') === flatName) {
const typeArgsStart = nameWithTypeArgs.indexOf('<');
if (typeArgsStart !== -1) {
return dottedName + nameWithTypeArgs.slice(typeArgsStart);
}
return dottedName;
}
}
// Check if we have a namespaced reference where the namespace itself needs transformation
// E.g., MenuRoot.Actions.Handler where MenuRoot → Menu.Root → Menu.Root.Actions.Handler
// This check comes BEFORE flat prefix matching to preserve namespace structure
if (typeName.namespaces && typeName.namespaces.length > 0) {
// Check if any namespace part is in the typeNameMap
const transformedNamespaces = typeName.namespaces.map(ns => typeNameMap[ns] || ns);
const hasTransformation = transformedNamespaces.some((ns, i) => ns !== typeName.namespaces[i]);
if (hasTransformation) {
// Build the transformed name: TransformedNamespace.Member
const transformedName = [...transformedNamespaces, typeName.name].join('.');
const typeArgsStart = nameWithTypeArgs.indexOf('<');
if (typeArgsStart !== -1) {
// Preserve type arguments
return transformedName + nameWithTypeArgs.slice(typeArgsStart);
}
return transformedName;
}
}
// Check if flatName starts with a known component prefix
// e.g., "ComponentPartState" starts with "ComponentPart" -> "Component.Part", so becomes "Component.Part.State"
// This handles types that don't have namespace structure (already flattened)
const sortedEntries = Object.entries(typeNameMap).sort((a, b) => b[0].length - a[0].length);
for (const [flat, dotted] of sortedEntries) {
if (flatName.startsWith(flat) && flatName.length > flat.length) {
const suffix = flatName.slice(flat.length);
const dottedName = `${dotted}.${suffix}`;
const typeArgsStart = nameWithTypeArgs.indexOf('<');
if (typeArgsStart !== -1) {
return dottedName + nameWithTypeArgs.slice(typeArgsStart);
}
return dottedName;
}
}
// No transformation needed - return as-is preserving namespace structure
// For external types like React.ComponentType, preserve the dotted format
if (typeName.namespaces && typeName.namespaces.length > 0) {
const dottedName = [...typeName.namespaces, typeName.name].join('.');
const typeArgsStart = nameWithTypeArgs.indexOf('<');
if (typeArgsStart !== -1) {
return dottedName + nameWithTypeArgs.slice(typeArgsStart);
}
return dottedName;
}
// Not in the map and no namespaces - it's an external type (React, HTMLElement, etc.)
return nameWithTypeArgs;
}
function createNameWithTypeArguments(typeName, exportNames, typeNameMap, externalTypesCollector, preserveTypeParameters) {
const prefix = typeName.namespaces && typeName.namespaces.length > 0 ? `${typeName.namespaces.join('.')}.` : '';
if (typeName.typeArguments && typeName.typeArguments.length > 0 && typeName.typeArguments.some(ta => ta.equalToDefault === false)) {
return `${prefix}${typeName.name}<${typeName.typeArguments.map(ta => formatType(ta.type, {
exportNames,
typeNameMap,
externalTypesCollector,
preserveTypeParameters
})).join(', ')}>`;
}
return `${prefix}${typeName.name}`;
}
/**
* When all members of a union are object types sharing common properties,
* extracts the common properties into an intersection to reduce repetition.
*
* Returns a formatted string like `({ reason: 'a' } | { reason: 'b' }) & { shared: X }`
* or `undefined` if merging is not applicable (not all objects, fewer than 2 common props, etc.).
*/
function mergeObjectUnionCommonProperties(members, options) {
const {
removeUndefined,
exportNames,
typeNameMap,
externalTypesCollector,
withPropertyComments,
preserveTypeParameters
} = options;
// Need at least 2 members for merging to make sense
if (members.length < 2) {
return undefined;
}
// Normalize members: flatten intersection-of-objects into virtual object nodes
// e.g., { a: X } & { b: Y } becomes { a: X; b: Y }
// This handles cases like `BaseUIChangeEventDetail<R> & { preventUnmountOnClose(): void }`
const objectMembers = [];
for (const member of members) {
if (isObjectType(member)) {
// Skip merging if the member is a named type (e.g., ProcessedParameter | ProcessedProperty)
// Named types should be rendered as type references, not expanded and merged.
if (!isAnonymousObjectType(member)) {
return undefined;
}
objectMembers.push(member);
} else if (isIntersectionType(member) && member.types.every(t => isObjectType(t))) {
// Skip if the intersection itself is a named type (e.g., ProcessedParameter = Omit<T, K> & { ... })
if (member.typeName && !isInternalTypeName(member.typeName.name)) {
return undefined;
}
const mergedProperties = member.types.flatMap(t => isObjectType(t) ? t.properties ?? [] : []);
objectMembers.push({
kind: 'object',
properties: mergedProperties
});
} else {
// Non-object, non-intersection-of-objects member — can't merge
return undefined;
}
}
// Collect all property names across all members
const allPropertyNames = new Set();
for (const member of objectMembers) {
for (const prop of member.properties ?? []) {
allPropertyNames.add(prop.name);
}
}
// Find properties that are common across ALL members (same name, type, and optionality)
const commonPropertyNames = new Set();
for (const propName of Array.from(allPropertyNames)) {
const propsAcrossMembers = objectMembers.map(member => (member.properties ?? []).find(p => p.name === propName));
// Must exist in ALL members
if (!propsAcrossMembers.every(p => p !== undefined)) {
continue;
}
// Must have the same optionality
if (!propsAcrossMembers.every(p => p.optional === propsAcrossMembers[0].optional)) {
continue;
}
// Must have the same formatted type
const formattedTypes = propsAcrossMembers.map(p => formatType(p.type, {
removeUndefined: p.optional,
exportNames,
typeNameMap,
externalTypesCollector,
preserveTypeParameters
}));
if (formattedTypes.every(t => t === formattedTypes[0])) {
commonPropertyNames.add(propName);
}
}
// Only merge when there are at least 2 common properties and unique properties remain
const hasUniqueProperties = commonPropertyNames.size < allPropertyNames.size;
if (commonPropertyNames.size < 2 || !hasUniqueProperties) {
return undefined;
}
// Format each union member with only unique (non-common) properties
const formatOpts = {
removeUndefined,
exportNames,
typeNameMap,
externalTypesCollector,
preserveTypeParameters
};
const uniqueUnionMembers = objectMembers.map(member => {
const uniqueProps = (member.properties ?? []).filter(p => !commonPropertyNames.has(p.name));
if (uniqueProps.length === 0) {
return '{}';
}
const parts = uniqueProps.map(m => {
const propertyName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(m.name) ? m.name : `'${m.name}'`;
const typeStr = formatType(m.type, {
...formatOpts,
removeUndefined: m.optional
});
return `${propertyName}${m.optional ? '?' : ''}: ${typeStr}`;
});
return `{ ${parts.join('; ')} }`;
});
// Deduplicate identical unique-property members
const dedupedUnionMembers = uniq(uniqueUnionMembers);
// Format the common properties as an intersection object
const firstMember = objectMembers[0];
const commonProps = (firstMember.properties ?? []).filter(p => commonPropertyNames.has(p.name));
const commonParts = commonProps.map(m => {
const propertyName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(m.name) ? m.name : `'${m.name}'`;
const typeStr = formatType(m.type, {
...formatOpts,
removeUndefined: m.optional,
withPropertyComments
});
const propLine = `${propertyName}${m.optional ? '?' : ''}: ${typeStr}`;
if (withPropertyComments && m.documentation) {
const comment = formatPropertyComment(m.documentation);
if (comment) {
return `${comment}\n${propLine}`;
}
}
return propLine;
});
const hasComments = withPropertyComments && commonParts.some(p => p.includes('/**'));
const commonSeparator = hasComments ? ';\n' : '; ';
const commonObject = `{ ${commonParts.join(commonSeparator)} }`;
const unionPart = dedupedUnionMembers.join(' | ');
return `(${unionPart}) & ${commonObject}`;
}
/**
* Looks for 'any', 'null' and 'undefined' types and moves them to the end of the array of types.
*/
function orderMembers(members) {
let orderedMembers = pushToEnd(members, 'any');
orderedMembers = pushToEnd(orderedMembers, 'null');
orderedMembers = pushToEnd(orderedMembers, 'undefined');
return orderedMembers;
}
function pushToEnd(members, name) {
const index = members.findIndex(member => {
return isIntrinsicType(member) && member.intrinsic === name;
});
if (index !== -1) {
const member = members[index];
return [...members.slice(0, index), ...members.slice(index + 1), member];
}
return members;
}
function isObjectEmpty(object) {
// eslint-disable-next-line
for (const _ in object) {
return false;
}
return true;
}
function normalizeQuotes(str) {
if (str.startsWith('"') && str.endsWith('"')) {
return str.replaceAll("'", "\\'").replaceAll('\\"', '"').replace(/^"(.*)"$/, "'$1'");
}
return str;
}
/**
* Formats a JSDoc comment block from property documentation.
* Returns undefined if no meaningful content to document.
*/
function formatPropertyComment(documentation) {
const lines = [];
if (documentation.description) {
lines.push(...documentation.description.split('\n'));
}
if (documentation.defaultValue !== undefined) {
lines.push(`@default ${String(documentation.defaultValue)}`);
}
// Include all tags preserved by tae (it already filters out @default,
// @private, @internal, @public, and @param during parsing).
// Split multi-line tag values so each line gets the ` * ` JSDoc prefix.
for (const tag of documentation.tags ?? []) {
if (tag.value) {
const tagLines = tag.value.split('\n');
if (tagLines.length > 1) {
// Multi-line values go on new lines after the tag name
lines.push(`@${tag.name}`);
for (const tagLine of tagLines) {
lines.push(tagLine);
}
} else {
lines.push(`@${tag.name} ${tagLines[0]}`);
}
} else {
lines.push(`@${tag.name}`);
}
}
if (lines.length === 0) {
return undefined;
}
if (lines.length === 1) {
return `/** ${lines[0]} */`;
}
return `/**\n${lines.map(line => ` * ${line}`).join('\n')}\n */`;
}