Roman Imankulov

Roman Imankulov

Full-stack Python web developer from Porto

search results (esc to close)
05 Jul 2022

My setup for a typescript react project

updated on 4 Dec 2022

A memo about my setup for a TypeScript React project: I copy and paste these commands whenever I need to start a new project and ensure that it has all linters, formatters, and pre-commit hooks configured.

Initialize proper version

I use nvm, and I automatically initialize the node environment with nvm if .nvmrc is found.

# File ~/.zshrc
test -f ./.nvmrc && nvm use

Start a new project

I tried Vite for a few recent projects and liked it. That’s how I create a new empty project.

yarn create vite  --template react-ts

Alternatively, here’s you can create a new project with create-react-app:

yarn create react-app my-app --template typescript

Define a node version

Use Node 18.x (aka Hydrogen) in a project.

echo lts/hydrogen > .nvmrc

Use Node 16.x (aka Gallium) in a project.

echo lts/gallium > .nvmrc

See Node codenames for more options.

Pinning versions

You can pin Node and NPM to a specific version.

Add to package.json

{
  "engines": {
    "node": "^18",
    "npm": "^8"
  }
}

Node is permissive, though, and by default it only raises a warning if engines don’t match. You can make node fail every time it tries to run the code in the environment that doesn’t match the pinned versions.

Create an .npmrc file.

echo "engine-strict=true" > .npmrc

See also.

Linters

What do we install:

Also, a bunch of plugins for to help ESLint validate your imports.

yarn add -D \
  eslint prettier \
  @typescript-eslint/parser @typescript-eslint/eslint-plugin \
  eslint-config-google eslint-config-prettier eslint-plugin-react \
  eslint-plugin-import eslint-import-resolver-typescript \
  eslint-plugin-no-relative-import-paths eslint-plugin-unused-imports

For the server-side minimal node-based setup without typescript and React.

yarn add -D eslint prettier eslint-config-google eslint-config-prettier

Add configuration file .eslintrc.cjs. Here we extend the recommended ESLint configuration with the rules from the extensions that we’ve installed above.

In the rules section, I overwrite some rules to my preferences. Usually, this sections grows with the project.

module.exports = {
  extends: [
    'google',
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:import/recommended',
    'plugin:react/recommended',
    'plugin:react/jsx-runtime',
    'prettier',
  ],
  plugins: [
    '@typescript-eslint',
    'unused-imports',
    'no-relative-import-paths',
    'import',
  ],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  env: {
    es6: true,
    node: true,
    browser: true,
  },
  rules: {
    'no-console': 'error',
    'require-jsdoc': 'off',
    'valid-jsdoc': 'off',
    'react/boolean-prop-naming': 'error',
    'react/sort-prop-types': 'error',
    'import/newline-after-import': ['error'],
    'unused-imports/no-unused-imports': 'error',

    // We need to disable "no-unused-vars" when working with TypeScript,
    // but plugin:@typescript-eslint/recommended is already doing that for us.
    // See https://typescript-eslint.io/rules/no-unused-vars/
    '@typescript-eslint/no-unused-vars': 'error',

    'import/order': [
      'error',
      {
        'newlines-between': 'always',
        alphabetize: {
          order: 'asc',
          caseInsensitive: true,
        },
        pathGroups: [
          {
            pattern: 'react',
            group: 'builtin',
            position: 'before',
          },
        ],
        pathGroupsExcludedImportTypes: ['react'],
      },
    ],
    'no-relative-import-paths/no-relative-import-paths': [
      'error',
      {
        allowSameFolder: true,
        rootDir: 'src',
      },
    ],
  },
  settings: {
    react: {
      version: 'detect',
    },
    'import/resolver': ['typescript'],
    // See https://stackoverflow.com/a/61839578
    'spaced-comment': ['error', 'always', { markers: ['/'] }],
  },
};

The light version of the same file for a server-side node-based setup without typescript and React and import linters.

module.exports = {
  extends: ['eslint:recommended', 'google', 'prettier'],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  env: {
    es6: true,
    node: true,
  },
  rules: {
    'require-jsdoc': 'off',
    'valid-jsdoc': 'off',
    'no-unused-vars': 'error',
  },
};

I use single quotes in JavaScript strings, so I add a single-quote configuration for prettier: .prettierrc.json:

echo '{ "singleQuote": true }' > .prettierrc.json

Install lint-staged and husky with mrm.

npx mrm@2 lint-staged

It installs dependencies and creates or updates necessary config files.

Double-check lint-staged configuration. You should have the following section in your package.json.

  "lint-staged": {
    "*.{js,jsx}": "eslint --cache --fix",
    "*.{js,jsx,css,md}": "prettier --write",
    "*.{ts,tsx}": "sh -c 'tsc --noEmit'"
  }

Configuring absolute import paths

Configuring “no-relative-import-paths” Linter can be a bit tricky. I use the configuration that considers the “src” directory as the source root, and all imports from the “src” directory are considered absolute.

Update automatically generated tsconfig.json and include baseUrl.

{
  "compilerOptions": {
    "baseUrl": "src"
  }
}

Teaching Vite understand absolute imports requires a separate plugin vite-tsconfig-paths. Install it with yarn.

yarn add -D vite-tsconfig-paths

Then update vite.config.js to use the plugin. I added tsconfigPaths to a currently existing react() plugin.

import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), tsconfigPaths()],
});

Verify current project

npx eslint --cache --fix src
npx prettier --write src

VSCode

VSCode should catch the Linter configuration for the project. If it doesn’t, simply restarting VSCode often helps.

Automate this guideline

Note to self: you can turn this memo into an mrm preset.

Roman Imankulov

Hey, I am Roman, and you can hire me.

I am a full-stack Python web developer who loves helping startups and small teams turn their ideas into products.

More about me and my skills