How do you use the Typescript compiler's Typechecker to get the resolved type when the types are defined in a different file?

185 views Asked by At

I'm creating a TypeScript plugin that collects type information and attaches it as a string to make it available runtime. Resolving the types works when the types are defined in the same file, but not when defined in a separate file. How can I get the resolved types in that case?

The TypeScript plugin (transformer) that I have now is:

import ts from 'typescript'

const transformer: ts.TransformerFactory<ts.SourceFile> = context => {
  const program = ts.createProgram([],  {})
  const checker = program.getTypeChecker()

  return sourceFile => {
    const visitor = (node: ts.Node): ts.Node => {
      // we're looking for a function call like $reflect(deps => ...)
      // then, we insert a second argument with a string holding the resolved type of the argument
      if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.escapedText === '$reflect') {
        // @ts-ignore
        const paramNode = node.arguments[0].parameters[0]
        const type = checker.getTypeAtLocation(paramNode)
        const paramType = checker.typeToString(type, undefined, ts.TypeFormatFlags.InTypeAlias)

        // @ts-ignore
        node.arguments.push(ts.factory.createStringLiteral(paramType))

        return node
      }

      return ts.visitEachChild(node, visitor, context)
    }

    return ts.visitNode(sourceFile, visitor)
  }
}

export default transformer

This works with the following code:

export function $reflect<T>(arg: T, types?: string) : T {
  if (!types) {
    console.error('$reflect: Error: types should be resolved with runtime type information')
  }
  console.log(`$reflect: runtime types: ${types}`)
  // TODO: use the types
  return arg
}

export interface Signatures<T> {
  add: (a: T, b: T) => T
  divide: (a: T, b: T) => T
  create: (a: unknown) => T
}

type Signature<Name extends SignatureKey<T>, T> = Signatures<T>[Name]
type SignatureKey<T> = keyof Signatures<T>
type Dependencies<Name extends SignatureKey<T>, T> = {[K in Name]: Signature<K, T>}

export const avg = $reflect(<T>(dep: Dependencies<'add' | 'divide' | 'create', T>): (values: T[]) => T =>
  values => {
    const sum = values.reduce(dep.add)
    return dep.divide(sum, dep.create(values.length))
  }
)
// Works
// Resolves the types as expected:
//   '{ add: (a: T, b: T) => T; divide: (a: T, b: T) => T; create: (a: unknown) => T; }'

But does not work when the types are defined in a separate file types.ts:

// types.ts
export interface Signatures<T> {
  add: (a: T, b: T) => T
  divide: (a: T, b: T) => T
  create: (a: unknown) => T
}

export type Signature<Name extends SignatureKey<T>, T> = Signatures<T>[Name]
export type SignatureKey<T> = keyof Signatures<T>
export type Dependencies<Name extends SignatureKey<T>, T> = {[K in Name]: Signature<K, T>}
// index.ts
import { Dependencies } from './types.js'

export function $reflect<T>(arg: T, types?: string) : T {
  if (!types) {
    console.error('$reflect: Error: types should be resolved with runtime type information')
  }
  console.log(`$reflect: runtime types: ${types}`)
  // TODO: use the types
  return arg
}

export const avg = $reflect(<T>(dep: Dependencies<'add' | 'divide' | 'create', T>): (values: T[]) => T =>
  values => {
    const sum = values.reduce(dep.add)
    return dep.divide(sum, dep.create(values.length))
  }
)
// Does not work
// Does not resolve the types:
//   'Dependencies<"add" | "divide" | "create", T>'
// Should be:
//   '{ add: (a: T, b: T) => T; divide: (a: T, b: T) => T; create: (a: unknown) => T; }'

I suspect I'm using checker.typeToString or getTypeAtLocation wrongly. How to resolve the types when they are defined in a separate file?

1

There are 1 answers

1
Michael Jonker On

Because your target node is being referenced from an external file, it will firstly be a 'ImportSpecifier' before going back to a 'Node'. You will need an extra step to get your handle back to the node itself.

Try something like this:

const aliasSymbol = checker.getSymbolAtLocation(node);
const paramSymbol = checker.getAliasedSymbol(aliasSymbol);
const paramNode = paramSymbol?.valueDeclaration?.arguments[0].parameters[0];
const type = checker.getTypeAtLocation(paramNode);
const paramType = checker.typeToString(type, undefined, ts.TypeFormatFlags.InTypeAlias);