Skip to content

Modus-Logo-Long-BlackCreated with Sketch.

  • Services
  • Work
  • Blog
  • Resources

    OUR RESOURCES

    Innovation Podcast

    Explore transformative innovation with industry leaders.

    Guides & Playbooks

    Implement leading digital innovation with our strategic guides.

    Practical guide to building an effective AI strategy
  • Who we are

    Our story

    Learn about our values, vision, and commitment to client success.

    Open Source

    Discover how we contribute to and benefit from the global open source ecosystem.

    Careers

    Join our dynamic team and shape the future of digital transformation.

    How we built our unique culture
  • Let's talk
  • EN
  • FR

TypeScript is becoming increasingly popular as more developers find it useful. Not only can you get strong typing support, but you can also use newer spec syntax. The former is one of the reasons we chose TypeScript in our Gimbal project, as typing support helps a dynamic team by showing errors early on and documenting what code should be. In this article, we will be discussing a generic TypeScript project that we will be publishing to the npm repository with an optional binary that can be executed in the command line.

Project Structure

The structure of any TypeScript project is key and can be easily configured. It’s common to place your TypeScript files in the src directory and have them compiled into the lib (can be configured to be dist) directory. By default, your custom types, when abstracted out of the individual files where they are used, would go into the typings directory and have the file extension of d.ts. There is nothing that says the directories must be named this, it’s just a common convention that has been adopted as a best practice across the industry.

A sample structure would be:

/
  lib/
    index.js
  src/
    index.ts
  typings/
    index.d.ts
  package.json
  tsconfig.json

You don’t need to create the lib directory. When you compile your TypeScript, this will be created for you in the above location.

Dependencies

Before we begin using TypeScript and compiling, we need to install some dependencies. You’ll only need three modules that should be installed as devDependencies so we’ll use the --save-dev option when installing them:

npm install --save-dev typescript ts-node @types/node

You’ll also need to tell npm which files to publish as we don’t want to publish the raw source, so that what is installed for users is as minimal as it can be. Notice that users can still access the full source code in the module’s repository. Npm will always publish some files like the README and LICENSE files (see the list here), so all you need to do is to add the lib directory to the files array in your package.json:

"files": [
  "lib"
]

I also tend to add two scripts to package.json:

"scripts": {
  "build": "tsc",
  "build:check": "tsc --noEmit",
}

The build script is what you would use when preparing to publish to npm. The build:check can be used to run a one-off test to see if your build would succeed without creating the JavaScript files in the lib directory.

tsconfig.json

TypeScript expects the tsconfig.json to be in the root of your project as shown in the sample structure. This file tells TypeScript where your source files are, where to compile to, where your typings are, and what kind of module resolution in the compiled JavaScript should be used, among other things. A simple example would be:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "lib": ["es6", "es2015"],
    "types": ["node"],
    "declaration": true,
    "outDir": "lib",
    "rootDir": "src",
    "strict": true,
  },
  "exclude": [
    "lib",
    "node_modules",
    "src/**/*.spec.ts"
  ],
  "include": [
    "src"
  ]
}

There are five options that are important and need to be pointed out:

  • outDir – This is the name of the directory of the compiled JavaScript. The compiled structure of this directory will be the same as the structure of the rootDir.
  • rootDir – The directory where the TypeScript source is.
  • types – An array of typing names to always load. In this example, it will use the @types/node module we installed before.
  • exclude – An array of glob paths to exclude from the TypeScript compilation.
  • include – An array of glob paths to include in the TypeScript compilation.

For more information on tsconfig.json, please see the TypeScript documentation. For more information on the options that can go into the compilerOptions object, see this TypeScript documentation including default values.

Adding a bin

If your project needs a binary executable that can be executed from the command line, you’ll need to add the bin directory to the exclude array in your tsconfig.json and add the bin config to your package.json. The bin directory will not be compiled with TypeScript so all files in there need to be in plain JavaScript.

An example of the bin config within your package.json is:

"bin": {
  "my-cli": "./bin/index.js"
},

Here is an example of the bin/index.js file:

#!/usr/bin/env node

const minimist = require('minimist');
const argv = minimist(process.argv.slice(2));

require('../lib')(argv);

We are using the require builtin function here since we are in a JavaScript file that has to work within Node.js. This binary will execute the compiled JavaScript; we execute it passing in the arguments from the command line parsed by minimist. This allows your project to also be executed programmatically, not just from the command line. Minimist is just one of the command line parsers but is minimal. If you need something more advanced you may want to check out commander or yargs.

The last step is to add the bin directory to the files array in package.json telling npm to publish it as well:

"files": [
  "bin",
  "lib"
]

Testing the bin

Now that you have your bin created, it’s time to test and debug using the bin. This is where the ts-node module comes in handy as you’ll need to execute the code while compilingTypeScript on the fly. I normally set up three start scripts in package.json, as follows:

"start": "node -r ts-node/register bin/index.js",
"start:break": "node --inspect-brk -r ts-node/register bin/index.js",
"start:debug": "node --inspect -r ts-node/register bin/index.js"

Each of these scripts do the same thing: they will compile your raw TypeScript on the fly using ts-node. The difference is whether you want to debug as the source executes or not. The start script will run your TypeScript without any debugging (other than console.log()s of course). The start:break and start:debug scripts will open a debugging port that allows you to connect an external debugger, such as Google Chrome or an editor. The difference between these two is start:break will not execute your TypeScript until you have attached an external debugger and resume execution, while start:debug will continue to run your source but you can still attach a debugger until the process ends. You’ll likely want to use start:break so you can attach the debugger and set any breakpoints unless you are working on an async operation, like a web server, that you want to get running.

You would execute one of these on the command line like this:

npm start

// or to pass arguments:
npm run start:break -- --foo bar

The second line passes all the options to the right of — to your node script since npm has its own options that could get used before your node script would get used. Your src/index.ts file would look like:

interface Arguments {
  foo: string;
}

const mymodule = (args: Arguments): void => {
  const { foo } = args;

  // ...
};

export default mymodule;

If you are using VSCode, you can use a launch config that looks very similar to the start script above and allows you to use VSCode’s built in debugging:

{
  "type": "node",
  "request": "launch",
  "name": "Test Bin",
  "runtimeArgs": [
    "-r",
    "ts-node/register"
  ],
  "args": [
    "${workspaceFolder}/bin/index.js",
    "--foo",
    "bar"
  ],
  "skipFiles": [
    "/**/*.js"
  ]
}

You can also use npm link along with the build:watch script in order to use your bin globally as if you installed it from a repository. This uses the compiled code which is why the build:watch script can be used to rebuild automatically.

Publishing to NPM

The first thing to notice is that, by default, npm attempts to publish as a private package. Unless you pay for and need this feature, you’ll have to set it to public. Add the following to your package.json:

"publishConfig": {
  "access": "public"
},

Now, when you execute npm publish, it will use this config and set the access to public. You can use npm publish --access publish if you do not wish to have this in your package.json, just make sure you don’t forget it (or include it as a CI step). The world will not end if you forget, but the publish will fail and you may have a commit and tag that you’ll have to clean up.

The flow to publish is basically:

npm version patch
npm run build
npm publish

While you can do this all locally, I would always recommend setting up a CI/CD pipeline to automate this. Then, all you would be responsible for is executing the npm version patch command, and once you push the tag up to the remote git repository via executing git push --follow-tags, your CI/CD will do the building and publishing for you. An example CircleCI .circleci/config.yml file looks like:

version: 2.1

jobs:
  publish:
    docker:
      - image: circleci/node:10
    steps:
      - checkout
      - run:
        name: Setup .npmrc credentials
        command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
      - run: npm ci
      - run: npm run build
      - run: npm publish

workflows:
  version: 2
  publish:
    jobs:
      - publish:
        filters:
          branches:
            ignore: /.*/
          tags:
            only: /^v.*/

Conclusion

TypeScript brings a lot of protection with the strong typing you can use. Many of today’s editors also have support for TypeScript showing you in-line hints and errors. In a world where anyone can submit a pull request to fix a bug or add a new feature, TypeScript is a natural fit as it can help catch bugs and increase developer productivity.

Posted in Application Development
Share this

Mitchell Simoens

Mitchell Simoens is a Senior Front End Engineer at Modus Create. Mitchell has spent the last 10 years working with Ext JS including developing core functionality, Sencha Fiddle and (I hope your insurance covers blown minds) supporting the online community with over 40,000 posts on the Sencha Forums. Before working with Ext JS, Mitchell used Perl and PHP but loves spending time with Node JS for today's needs. When not working, you can find Mitchell relaxing with his wife and daughter, or developing his talents as an amateur furniture maker.
Follow

Related Posts

  • Swift-Modules-for-React-Native
    Swift Modules for React Native

    React Native is an Objective-C application framework that bridges JavaScript applications running in the JSCore…

  • Swift-Modules-for-React-Native
    Swift Modules for React Native

    React Native is an Objective-C application framework that bridges JavaScript applications running in the JSCore…

Want more insights to fuel your innovation efforts?

Sign up to receive our monthly newsletter and exclusive content about digital transformation and product development.

What we do

Our services
AI and data
Product development
Design and UX
IT modernization
Platform and MLOps
Developer experience
Security

Our partners
Atlassian
AWS
GitHub
Other partners

Who we are

Our story
Careers
Open source

Our work

Our case studies

Our resources

Blog
Innovation podcast
Guides & playbooks

Connect with us

Get monthly insights on AI adoption

© 2025 Modus Create, LLC

Privacy PolicySitemap
Scroll To Top
  • Services
  • Work
  • Blog
  • Resources
    • Innovation Podcast
    • Guides & Playbooks
  • Who we are
    • Careers
  • Let’s talk
  • EN
  • FR