Search code examples
typescripttypescript-compiler-api

TypeChecker API: How do I find inferred type arguments to a function?


Is there any way to, given a CallExpression with inferred type arguments, find what those type arguments are?

Example code:

class SomeClass {
    public someMethod<T>(arg: T): void { }
}

// What is the inferred type of T in this call?
someClass.someMethod(7);

It's easy enough to find type arguments that were explicitly assigned in the code, but I can't figure out how to find what was inferred.

function inferTypeArguments(node: ts.CallExpression, typeChecker: ts.TypeChecker) {
    node.typeArguments; // is empty
    const signature = typeChecker.getResolvedSignature(node);
    signature['typeArguments']; // is also empty

    // This returns "<number>(arg: number): void"
    // so I know that the typeChecker has the right information,
    // but I would really like a ts.Type[]
    typeChecker.signatureToString(signature, node, ts.TypeFormatFlags.WriteTypeArgumentsOfSignature)
}

Solution

  • I'd been using Simon's code for a while in my transpiler... then 3.9 came along and broke it. I've done a preliminary attempt to get it working again. Unfortunately, the mapper is an "internal" concern for typescript, so this will likely change again in the future

    /* @internal - from typescript 3.9 codebase*/
    const enum TypeMapKind {
      Simple,
      Array,
      Function,
      Composite,
      Merged,
    }
    
    /* @internal - from typescript 3.9 codebase*/
    type TypeMapper =
      | { kind: TypeMapKind.Simple, source: ts.Type, target: ts.Type }
      | { kind: TypeMapKind.Array, sources: readonly ts.Type[], targets: readonly ts.Type[] | undefined }
      | { kind: TypeMapKind.Function, func: (t: ts.Type) => ts.Type }
      | { kind: TypeMapKind.Composite | TypeMapKind.Merged, mapper1: TypeMapper, mapper2: TypeMapper };
    
    /* basic application of the mapper - recursive for composite.*/ 
    function typeMapper(mapper:TypeMapper, source: ts.Type): ts.Type {
      switch(mapper.kind){
        case TypeMapKind.Simple: 
          return mapper.target;
        case TypeMapKind.Array:
          throw Error("not implemented");
        case TypeMapKind.Function: 
          return mapper.func(source);      
        case TypeMapKind.Composite: 
        case TypeMapKind.Merged:
          return typeMapper(mapper.mapper2, source);
      }
    }
    
    function inferTypeArguments(node: ts.CallExpression, typeChecker: ts.TypeChecker): ts.Type[] {
      const signature:ts.Signature = typeChecker.getResolvedSignature(node);
      const targetParams: ts.TypeParameter[] = signature['target'] && signature['target'].typeParameters;
    
      if (!targetParams) {
        return [];
      }
    
      if(signature['mapper'] == undefined)   
        return targetParams;   
    
      //typescript <= 3.8
      if(typeof signature['mapper'] == "function") 
        return targetParams.map(p=>signature['mapper'](p));
      //typescript >= 3.9.... 
      return targetParams.map(p=> typeMapper(signature['mapper'] as TypeMapper, p));
    }