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?
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: