Daniel
Daniel

Reputation: 2767

Emit an inferred mapped type to a .d.ts

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.

Upvotes: 3

Views: 563

Answers (1)

Gerrit0
Gerrit0

Reputation: 9192

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.

Upvotes: 3

Related Questions