3

I have a mapped type called Commands in file, like this:

// File: protocol.ts

import { CArgs, CRes } from '../utils';

type S = import('src/applications').Services;
export type Commands = {
    -readonly [P in keyof S]: {
        args: CArgs<S[P]>;
        res: CRes<S[P]>;
    };
};

// ... more declarations

The inference is quite complex, Services is by itself a mapped type that is inferred using UnionToIntersection and more fancy things, with types that come from many different files... But it works and the type as inferred by TypeScript is something like this:

type Commands = {
    Com1: {
        args: number;
        res: void;
    };
    Com2: {
        args: {foo: string};
        res: string[];
    };
    // ... etc ...
};

Now I want to emit a single .d.ts file with the resolved type, as shown in the second snippet, so clients doesn't need declarations for the entire tree of files to infer the type. In other words I'd like to "paste" the type as inferred from TypeScript in place, replacing Commands declaration and removing all import statements.

I think I need to use the compiler API but the small documentation I could find isn't very clear. To be honest, I don't even know where to start.

How do I perform that task??

I started by creating a Program using ts.createProgram() giving the filename protocol.ts. I visited every node from there so I know the file is loaded. I created a TypeChecker and used .getTypeAtLocation() for almost every Node, logging checker.typeToString(type), checker.GetFullyQualifiedName(type.symbol), and many more, but there is no trace of the inferred type. Maybe the type checker or the program needs to have every file loaded? (I supposed it'd do it for itself) If so, how do I create the array from the glob pattern in the tsconfig.json?

As you can see I'm a little lost so any help and orientation is welcome.

Thank you very much in advance.

Daniel
  • 2,657
  • 1
  • 17
  • 22
  • Old question, but did you ever find a reliable solution for this? I have the exact same issue. The below works for some examples, but just like vscode, has truncation for larger ones and does expand unions. I can add some tricks like those in this answer, but then it deep dives too far for things like `dayjs`, etc. https://stackoverflow.com/a/57683652/1057157 – Eric Haynes Jul 18 '21 at 02:13
  • 1
    @erich2k8 No, not really. In the end I just throw up this idea for this particular project. I'm still curious how to solve it properly, but doesn't seem very straightforward to do. – Daniel Jul 19 '21 at 18:26

1 Answers1

2

I'm almost certain there's a better way to do this, but following the incremental example for the compiler API I was able to get the same information that VSCode displays.

Given ./src/mapped-types.ts is:

type A = { a: 1, b: 2, c: 3 }

export type Mapped = {
    readonly [K in keyof A]: A[K]
}

The following code will print out:

type Mapped = {
    readonly a: 1;
    readonly b: 2;
    readonly c: 3;
}
import * as ts from 'typescript'
import * as fs from 'fs'

const rootFileNames = ['./src/mapped-types.ts']

const files: ts.MapLike<{ version: number }> = {};

const servicesHost: ts.LanguageServiceHost = {
    getScriptFileNames: () => rootFileNames,
    getScriptVersion: fileName => files[fileName] && files[fileName].version.toString(),
    getScriptSnapshot: fileName => {
        if (!fs.existsSync(fileName)) {
            return undefined;
        }

        return ts.ScriptSnapshot.fromString(fs.readFileSync(fileName).toString());
    },
    getCurrentDirectory: () => process.cwd(),
    getCompilationSettings: () => ({}),
    getDefaultLibFileName: options => ts.getDefaultLibFilePath(options),
    fileExists: ts.sys.fileExists,
    readFile: ts.sys.readFile,
    readDirectory: ts.sys.readDirectory
};

// Create the language service files
const services = ts.createLanguageService(servicesHost, ts.createDocumentRegistry());

const index = fs.readFileSync(rootFileNames[0], 'utf-8').indexOf('Mapped')
const info = services.getQuickInfoAtPosition(rootFileNames[0], index)!
console.log(ts.displayPartsToString(info.displayParts))

The relevant code for getting the quick info is here, unfortunately there's a lot going on there that I don't understand, so I'm not sure how to point you in the right direction.

Gerrit0
  • 7,955
  • 3
  • 25
  • 32
  • Thank you for your answer. I'll been out but I'll continue this work ASAP. I tried `ts-morph` and left a [question](https://github.com/dsherret/ts-morph/issues/758) there. As I said there, the code almost work but I'm getting some references to other types names when they're nested, instead of getting the full plain type as written inline. I'll explore your suggestions and let you know if I achieve something of interest, just need some more time to finish other things. Happy holidays. – Daniel Dec 23 '19 at 16:16