This is a template project that combines Typescript, React, Jest, and Vite.
There are many similar template projects available.
Vite, for example, has a community template repository with dozens of templates covering many combinations of tools.
The problem with these templates, and tools like create-react-app
, is that they provide code and configuration but generally offer no explanation.
This small project attempts to remedy that.
It provides a baseline configuration along with explanations of the important settings.
The stack is:
- Node.js version 18
- pnpm version 8
- Typescript version 5
- React version 18
- Jest version 29
- Vite version 4
- SWC version 1
This combination presents some challenges. In particular, pnpm and Typescript disagree on how type definitions work for transitive dependencies.
-
Create
package.json
{ "name": "vite-react", "version": "0.1.0", "scripts": { }, "engines": { "node": ">=18" }, "private": true }
-
Add the basic dependencies for the project:
react
,react-dom
, andtypescript
.$ pnpm add react react-dom $ pnpm add -D typescript
-
Create
tsconfig.json
.{ "compilerOptions": { "module": "es2022", "moduleResolution": "bundler", "noEmit": true, "jsx": "preserve", "lib": [ "DOM", "DOM.Iterable", "ES2021" ], "target": "es2020", "useDefineForClassFields": true } }
There are hundreds of settings available in
tsconfig.json
. These are the absolute minimum required:module
:"es2022"
: This sets the module system supported by Typescript. The value"es2022"
enables ECMAScript modules, dynamic import, andimport.meta
.moduleResolution
:"bundler"
: This sets the strategy that Typescript uses to find modules. The value"bundler"
matches SWC's module resolution strategy.noEmit
:true
: This stops the Typescript compiler from emitting compiled JavaScript files. SWC will generate JavaScript instead.jsx
:"preserve"
: This enables support for JSX syntax and sets how it is transformed before the compiler checks the program. The value"preserve"
enables JSX support but without any transformation. This is semantically correct since we are using SWC and no transformation is necessary. Other values may work here, but they are particularly sensitive to the rest of the project's configuration.lib
:["DOM", "DOM.Iterable", "ES2021"]
: By default, Typescript only supports the most basic APIs of JavaScript. You must enumerate other APIs here. The values listed here are the minimum required by the sample program. A complete application may require additional libraries.target
:"es2020"
: The TSConfig Reference describes this as the version of JavaScript used in the output files generated by the compiler. But that description seems incomplete. Despite thatnoEmit
is set, and no files are generated,"target"
must be set to"es6"
/"es2015"
or higher, or the sample program will not compile. The value"es2020"
simply matches the value used by Vite's React plugin in development mode.useDefineForClassFields
:true
: This sets how the Typescript compiler emits class fields. Vite's React plugin always sets this totrue
. It's not absolutely required, but I choose to set it so that runningtsc
will be consistent with runningvite
. -
Add a
check
script topackage.json
.--- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "vite-react", "version": "0.1.0", "scripts": { + "check": "tsc" }, "engines": { "node": ">=18"
-
Run the
check
script to type check the program.$ pnpm run check
-
Add the dependencies required for unit tests.
$ pnpm add -D @testing-library/jest-dom @testing-library/react jest jest-environment-jsdom ts-node
-
Create
jest.config.ts
and configure Jest to use thejsdom
test environment.import { Config } from 'jest' const config: Config = { testEnvironment: 'jsdom', } export default config
-
The Jest global functions are not automatically available in your tests. A side effect import installs the functions into the test environment. You can put this import in every single test file, or you can put it in a single file that Jest evaluates before every test.
Create a file
setup-jest.ts
that contains the side effect import.import '@testing-library/jest-dom/extend-expect'
Configure Jest to evaluate that file before each test.
--- a/jest.config.ts +++ b/jest.config.ts @@ -1,6 +1,9 @@ import { Config } from 'jest' const config: Config = { + setupFilesAfterEnv: [ + '<rootDir>/setup-jest.ts' + ], testEnvironment: 'jsdom', }
This mechanism can be used to configure the test environment in other ways. It is commonly used to polyfill functions that are not natively supported by Node (e.g.
fetch
andstructuredClone
for older versions of Node.) -
The Jest global functions are now available, but their type definitions are not.
@testing-library/jest-dom
doesn't include the type definitions directly. It references them from a separate package which is listed as a dependency. pnpm installs that package, but not in a location where Typescript will find its type definitions.This issue isn't specific to
@testing-library/jest-dom
. It applies to any project that ships type definitions as a transitive dependency.There are at least two solutions:
-
You can add the necessary type definitions package directly to your project.
$ pnpm add -D @types/testing-library__jest-dom
-
You can use customized pnpm settings to "hoist" transitive dependencies to the root
node_modules
where Typescript will find them. Add these lines to your project's.npmrc
.public-hoist-pattern[]=*eslint* public-hoist-pattern[]=*prettier* public-hoist-pattern[]=@types*
All three lines are required. The first two (eslint and prettier) are pnpm's defaults that are otherwise discarded when you configure this setting. The third line hoists all
@types
packages.You will have to rebuild the
node_modules
directory after you make this change.$ pnpm install
I recommend the first solution. It's simple and explicit. Hoisting works, but it breaks module isolation and can lead to unintended side effects.
-
-
Jest does not natively understand Typescript nor React. Add
@swc/jest
.$ pnpm add -D @swc/jest
Configure Jest to transform source files with
@swc/jest
. SWC automatically supports Typescript, but not React. Set thejsc.transform.react.runtime
to enable support for React. The value'automatic'
uses the modernreact/jsx-runtime
API and matches the value used by Vite's React plugin. This is the minimum configuration required.--- a/jest.config.ts +++ b/jest.config.ts @@ -5,6 +5,20 @@ const config: Config = { '<rootDir>/setup-jest.ts', ], testEnvironment: 'jsdom', + transform: { + '^.+\\.(js|jsx|ts|tsx)$': [ + '@swc/jest', + { + jsc: { + transform: { + react: { + runtime: 'automatic', + }, + }, + }, + }, + ], + }, } export default config
One important note: the configuration above only applies to Jest and is only sufficient for running tests under Jest. It has no effect on Vite and these settings are not sufficient for Vite. Vite's configuration is described later.
-
Add a
test
script topackage.json
.--- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "scripts": { "check": "tsc", + "test": "jest" }, "engines": { "node": ">=18"
-
Run the tests with Jest.
$ pnpm run test
-
Add Vite and it's React plugin.
$ pnpm add -D vite @vitejs/plugin-react-swc
Note that there are two React plugins for vite. Use the "swc" one. (The other one uses Babel.)
-
Create
vite.config.ts
import { defineConfig } from 'vite' import react from '@vitejs/plugin-react-swc' export default defineConfig({ plugins: [ react(), ], })
The Vite React plugin configures both Vite and SWC to support React. The plugin has some caveats and offers only limited configuration options.
As described earlier, this SWC configuration is separate from the one used for Jest. In principle, you could use a single
.swcrc
to configure both. I have not yet found a case where this is necessary. The limitations of the Vite React plugin will complicate this. -
Add
build
andstart
scripts topackage.json
.--- a/package.json +++ b/package.json @@ -2,7 +2,9 @@ "name": "vite-react", "version": "0.1.0", "scripts": { + "build": "vite build", "check": "tsc", + "start": "vite", "test": "jest" }, "engines": {
-
Run the vite development server.
$ pnpm start
-
Build the project with vite.
$ pnpm run build