import { Type , TypeChecker } from "ts-morph" ; export interface DecompositionResult { /**
     * The decomposed type with external types replaced with strings
     */ typeAsString : string ; /**
     * External types that have been replaced
     */ replacedTypes : Set < string > ; /**
     * Generic classes that are required for the class because
     * they can't be replaced without disrupting type structure
     * Mapping is name to number of type params
     */ requiredGenerics : GenericsInfo ; } /**
 * Class to track information on generic classes found during type decomposition
 * Actual breaking change detection of those classes is handled where they are
 * exported
 */ export class GenericsInfo extends Map < string , number > { static builtIns : string [ ] = [ "Array" , "Promise" , "Map" , "Set" ] ; set ( key , value ) { if ( GenericsInfo . builtIns . findIndex ( ( v ) => v === key ) !== - 1 ) { return this ; } const oldValue = this . get ( key ) ?? 0 ; return super . set ( key , Math . max ( value , oldValue ) ) ; } merge ( from ) { from . forEach ( ( v , k ) => this . set ( k , v ) ) ; } } /**
 * Merge multiple DecompositionResults where the types as strings are concatentated with the
 * separator and extracted types sets are merged
 */ function mergeResults ( into , from , separator ) { if ( into . typeAsString === undefined ) { into . typeAsString = from . typeAsString ; into . replacedTypes = from . replacedTypes ; into . requiredGenerics = from . requiredGenerics ; } else { into . typeAsString = `${ into . typeAsString }${ separator }${ from . typeAsString }` ; from . replacedTypes . forEach ( ( v ) => into . replacedTypes ! . add ( v ) ) ; into . requiredGenerics ! . merge ( from . requiredGenerics ) ; } } param typeChecker param type returns export function typeToString ( typeChecker , type ) { return typeChecker . compilerObject . typeToString ( type . compilerType ) ; } param checker param node returns export function decomposeType ( checker , node ) { const result = { typeAsString : typeToString ( checker , node ) , replacedTypes : new Set < string > ( ) , requiredGenerics : new GenericsInfo ( ) , } ; if ( node . isLiteral ( ) || node . isBoolean ( ) ) { return result ; } if ( node . getAliasSymbol ( ) || node . isEnum ( ) ) { result . typeAsString = `"${ result . typeAsString }""` ; } if ( node . isTypeParameter ( ) ) { return result ; } node = node as Type ; if ( node . isUnion ( ) ) { return decomposeTypes ( checker , node . getUnionTypes ( ) , " | " ) ; } else if ( node . isIntersection ( ) ) { return decomposeTypes ( checker , node . getIntersectionTypes ( ) , " & " ) ; } else { const typeArgs = node . getTypeArguments ( ) ; if ( typeArgs . length > 0 ) { const typeArgsResult = decomposeTypes ( checker , typeArgs , ", " ) ; const symbolName = checker . compilerObject . symbolToString ( node . compilerType . getSymbol ( ) ! ) ; typeArgsResult . requiredGenerics . set ( symbolName , typeArgs . length ) ; typeArgsResult . typeAsString = `${ symbolName }<${ typeArgsResult . typeAsString }>` ; return typeArgsResult ; } else { result . typeAsString = `"${ result . typeAsString }"` ; return result ; } } } param checker param nodes param separator returns export function decomposeTypes ( checker , nodes , separator ) { const result = { } as DecompositionResult ; nodes . map ( ( t ) => { const subResult = decomposeType ( checker , t ) ; mergeResults ( result , subResult , separator ) ; } ) ; return result ; }