Defining Typescript types for my own code in a different folder

Viewed 723

I'm creating a library where I want to autogenerate Typescript types for the user's code. For that I would love to put the type definitions in a different folder, so that they are out of sight, but the IDE can still pick them up.

This is possible with Typescript modules, but I couldn't find a way to define types for an "ambient" module. A simplified code example for how I would like the file structure to be:

    // src/pages/WelcomePage.ts
    export const message = 'hi'
    // src/index.ts
    import { message } from './pages/WelcomePage'
    console.log(message) // message should be of type "Message", not of type "string".
    // types/pages/WelcomePage.d.ts
    // This doesn't work.
    // Can we make it work somehow?
    declare module "pages/WelcomePage" {
        type Message = string
        export const message: Message
    }

tldr: I would like all my generated types to be in types/... and they should define types for src/....

2 Answers

Perhaps I am not understanding the question fully, but this seems like something that could be done with a custom tsconfig and npm command. The tsconfig specifies the input and output directories, as well as that you only want declaration files:

// tsconfig.special.json:

{
    "compilerOptions": {
        "compiler_options": "go_here",
        "emitDeclarationOnly": true, // will only emit .d.ts files
        "outDir": "types"
    },
    "include": ["src/**/*.ts"],
    "exclude": ["node_modules"]
}

So all .ts files within the src directory will be compiled through typescript, but only .d.ts files will be emitted (not actual js), and they will end up in the types directory.

Now your npm command under "scripts" might look something like this:

"compile": "tsc --p tsconfig.special.json"

You said this is a library that other developers will install. If that's the case, they will have to create their own npm script to run this one. How to do that is discussed in How do I run an npm script of a dependent package.

You may need to tweak your tsconfig to make sure the src folder that typescript is reading from is indeed the user_project/src, not user_project/node_modules/your_library/src. If you want the library user's ide to pick up the newly generated declarations, the same goes for the output dir. It needs to output to user_project/types, not user_project/node_modules/your_library/types. If you need help accounting for that in your tsconfig / npm command, please make a comment and I can elaborate. Your use may also need to be informed that they will need to add the generated /types directory to their project's tsconfig.json, so make sure the generated .d.ts files are picked up.

Hopefully I'm not totally off the mark on what you're asking.

I managed to get it working using path aliases combined with following answer: https://stackoverflow.com/a/53014974/8456680

My solution using the "transformers" variant from said answer, combined with @ as alias for src directory:

// src/pages/WelcomePage.ts
export const message = "aaa";
// src/index.ts
import { message } from "@/pages/WelcomePage"
console.log(message)
// types/pages/WelcomePage.d.ts
declare module "@/pages/WelcomePage" {
    type Message = string
    export const message: Message
}

And project setup: tsconfig.json

{
    "compilerOptions": {
        "module": "CommonJS",
        "moduleResolution": "Node",
        "outDir": "./dist",
        "baseUrl": ".",
        "paths": {
            "@/*": ["src/*"]
        },
        "plugins": [
            {
                "transform": "@zerollup/ts-transform-paths",
                "exclude": ["*"]
            }
        ]
    },
    "include": ["./types/**/*", "./src/**/*"]
}

package.json

{
    "scripts": {
        "build": "ttsc -p ."
    },
    "devDependencies": {
        "@zerollup/ts-transform-paths": "^1.7.18",
        "ttypescript": "^1.5.13",
        "typescript": "~4.4.0"
    }
}

How does it work? The declaration files are included first, resulting in declaration of @/pages/WelcomePage module, which is then used by TS for checking during import in index.ts.

During compilation the ts-transform-paths is executed and rewrites the path @/pages/WelcomePage to resolved ./pages/WelcomePage making sure the resulting code works.

This solution however has a drawback: Noone is checking if src/pages/WelcomePage.ts actually exports what types/pages/WelcomePage.d.ts says it does.

Related