A Better Development Experience with TypeScript and TSConfig

- Lire en français
Resources: huchet-vue

Since the beginning of this series, we've been working with TypeScript by creating .ts files and using lang="ts" in the script section of Vue.js components.

However, we haven’t configured TypeScript in our project yet, so we’re not leveraging its full potential. Additionally, one of the benefits of using TypeScript in a library is to expose types to the developers who will utilize it. This enables type checking and auto-completion in their IDE.

Tip

Check the dist folder in packages/huchet-vue. You’ll notice only .js files are present. We need to generate .d.ts files to expose the types.

Configuring TypeScript

Let's start by installing TypeScript and the necessary Node.js types in packages/huchet-vue:

bash
pnpm add -D typescript @types/node

Thanks to the extensive Vite plugin ecosystem, we can use the vite-plugin-dts plugin to generate .d.ts files for our library.

Install it:

bash
pnpm add -D vite-plugin-dts

Next, update the vite.config.ts file in packages/huchet-vue:

ts
import Vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
import Dts from 'vite-plugin-dts'

export default defineConfig({
  plugins: [
    Vue(),
    Dts(), 
  ],
  build: {
    // ...
  }
})

Does it work now? Not yet. If it were that simple, I wouldn't write an entire article about it!

When using TypeScript, we have to configure it through a tsconfig.json file. This file includes numerous options to inform TypeScript about the project type, including global types and variables, file paths, and even the desired code compilation method.

Writing this the first time can be tedious, but fortunately, some projects provide a base configuration to extend from.

Another issue with tsconfig.json is that a package can have more than one. Our vite.config.ts file and .vue files have different contexts. The first needs access to the Node.js API, while the second needs access to the DOM and browser API.

We can resolve this by using a single tsconfig.json file with the references option. This allows us to split a TSConfig file into multiple files, each targeting a specific context.

Here’s the plan:

  1. tsconfig.json to combine the two contexts.
  2. tsconfig.node.json for the Node.js context.
  3. tsconfig.app.json for the Vue.js context.

The Node TypeScript Context

Create the tsconfig.node.json file in packages/huchet-vue:

bash
touch tsconfig.node.json

Populate it with the following:

json
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
    "target": "ES2022",
    "lib": ["ES2023"],
    "moduleDetection": "force",
    "types": ["node"],
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "allowImportingTsExtensions": true,
    "strict": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noEmit": true,
    "isolatedModules": true,
    "skipLibCheck": true,
    "noUncheckedSideEffectImports": true
  },
  "include": ["vite.config.ts"]
}

This file is similar to what's provided in any Vite template, except we've added the types option set to ["node"] to ensure Node.js types are available in the Vite configuration file. This configuration targets only the vite.config.ts file.

This forms our first TypeScript context.

The Vue.js TypeScript Context

The second context will target the Vue.js files and TypeScript files from our src folder, named tsconfig.app.json.

Create the tsconfig.app.json file:

bash
touch tsconfig.app.json

Include the following content:

json
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "include": [
    "src/**/*.ts",
    "src/**/*.vue"
  ]
}

Before using this setup, install the Vue.js TypeScript base configuration:

bash
pnpm add -D @vue/tsconfig

Vue.js offers a base configuration to expand on, crucial because TypeScript evolves rapidly, and Vue.js requires specific configurations. Writing a TypeScript configuration from scratch isn’t appealing to most developers.

Tip

I recommend examining the tsconfig.json file in the @vue/tsconfig package. It's well documented and offers valuable insights.

Our configuration extends the Vue.js setup with the extends option and targets the src folder using the include option. It's straightforward and functional.

This is our second TypeScript context.

The Global TypeScript Configuration

Now, create the tsconfig.json file to unify the two contexts. This file doesn't define a new context or compiler options but references the two other contexts for the TypeScript compiler to use as needed.

Create the tsconfig.json file:

bash
touch tsconfig.json

Include the following content:

json
{
  "files": [],
  "references": [
    {
      "path": "./tsconfig.app.json"
    },
    {
      "path": "./tsconfig.node.json"
    }
  ]
}

The empty files array is crucial to avoid including every project file. Each reference has its own context, so we must ensure that only one context targets a file.

This forms our global TypeScript configuration.

Tip

To verify everything works correctly, attempt to import a Node.js module in vite.config.ts where it should be accessible and in a Vue.js file where it should not be.

Generating the Types

With our TypeScript configurations set up, we can proceed to generate the types for our library.

Since we are combining two contexts, the vite-plugin-dts plugin needs explicit context direction.

Update the vite.config.ts file in packages/huchet-vue:

ts
import { resolve } from 'node:path'
import Vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
import Dts from 'vite-plugin-dts'

export default defineConfig({
  plugins: [
    Vue(),
    Dts({
      tsconfigPath: resolve(__dirname, 'tsconfig.app.json'),
    })
  ],
  build: {
    // ...
  },
})

We use the tsconfig.app.json file since Vite builds Vue.js and TypeScript files from the src folder.

Proceed to generate the types:

bash
pnpm run build

In our dist folder, a types folder containing the .d.ts files should appear.

txt
dist
├── index.d.ts
├── index.mjs
└── Button
    ├── Button.vue.d.ts
    └── index.d.ts

Exposing the Types

Having generated the types, the next step is to expose them to developers using our library.

Update the package.json file in packages/huchet-vue:

json
{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs"
    }
  },
  "main": "./dist/index.mjs",
  "types": "dist/index.d.ts"
}

We set the types field to point to the index.d.ts file in the dist folder, ensuring types are available in the IDE when developers import our library.

Important

Ensure the types field precedes any other exports in the package.json file.

Profil Picture of Estéban

Thanks for reading! My name is Estéban, and I love to write about web development.

I've been coding for several years now, and I'm still learning new things every day. I enjoy sharing my knowledge with others, as I would have appreciated having access to such clear and complete resources when I first started learning programming.

If you have any questions or want to chat, feel free to comment below or reach out to me on Bluesky, X, and LinkedIn.

I hope you enjoyed this article and learned something new. Please consider sharing it with your friends or on social media, and feel free to leave a comment or a reaction below—it would mean a lot to me! If you'd like to support my work, you can sponsor me on GitHub!

Continue readingAutomatic Component Testing with Vitest and Testing Library