【React Testing Library × Chakra UI】「Cannot find module '@/lib/todo' or its corresponding type declarations.」の解決方法

# はじめに

お疲れ様です、りつです。

Viteで「React × TypeScript」のプロジェクトを作成し、テストを実装しようとしたところ、表題のエラーに遭遇しました。

# 問題

jestやReactTestingLibrary導入後、`npm run test`実行時に以下のエラーが発生しました。

## エラー内容
```terminal:ターミナルのエラー内容
$ npm run test

> type-class@0.0.0 test
> jest

(node:412914) ExperimentalWarning: CommonJS module /home/ritsu/workspace/type-class/node_modules/jest-util/build/requireOrImportModule.js is loading ES Module /home/ritsu/workspace/type-class/jest.config.js using require().
Support for loading ES Module in require() is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
ts-jest[config] (WARN) message TS151001: If you have issues related to imports, you should consider setting `esModuleInterop` to `true` in your TypeScript configuration file (usually `tsconfig.json`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information.
 FAIL  src/__tests__/App.spec.tsx
  ● Test suite failed to run

    src/App.tsx:3:29 - error TS2307: Cannot find module '@/lib/todo' or its corresponding type declarations.

    3 import { GetAllTodos } from "@/lib/todo"
                                  ~~~~~~~~~~~~
    src/App.tsx:4:22 - error TS2307: Cannot find module '@/domain/todo' or its corresponding type declarations.

    4 import { Todo } from "@/domain/todo"
                           ~~~~~~~~~~~~~~~

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        3.834 s
Ran all test suites.
(node:412914) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
```

## ソースコード

```react:src/__tests__/App.spec.tsx
import { ChakraProvider, defaultSystem } from "@chakra-ui/react";
import App from "../App";
import { render, screen } from "@testing-library/react";

describe("App", () => {
  it("タイトルがあること", () => {
    render(<ChakraProvider value={defaultSystem}><App /></ChakraProvider>);
    const title = screen.getByTestId("title");

    expect(title).toBeInTheDocument();
  });
});
```

```react:src/App.tsx
import { useEffect, useState } from "react"
import { Table } from "@chakra-ui/react"
import { GetAllTodos } from "@/lib/todo"
import { Todo } from "@/domain/todo"

function App() {
  const [todos, setTodos] = useState<Todo[]>([]);

  useEffect(() => {
    const getAllTodos = async () => {
      const todoData =  await GetAllTodos();
      setTodos(todoData);
    }

    getAllTodos();
  }, [])

  return (
    <>
      <h1 data-testid="title">TODOリスト</h1>
      <Table.Root size="sm">
        <Table.Header>
          <Table.Row>
            <Table.ColumnHeader>Title</Table.ColumnHeader>
            <Table.ColumnHeader>Done</Table.ColumnHeader>
            <Table.ColumnHeader>CreatedAt</Table.ColumnHeader>
          </Table.Row>
        </Table.Header>
        <Table.Body>
          {todos.map((todo) => (
            <Table.Row key={todo.id}>
              <Table.Cell>{todo.title}</Table.Cell>
              <Table.Cell>{todo.done ? "TRUE" : "FALSE"}</Table.Cell>
              <Table.Cell>{todo.created_at}</Table.Cell>
            </Table.Row>
          ))}
        </Table.Body>
      </Table.Root>
    </>
  )
}

export default App
```

```json:tsconfig.app.json
{
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
    "target": "ESNext",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "Bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedSideEffectImports": true,

    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src"]
}
```

```json:tsconfig.json
{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ],
  "compilerOptions": {
    "jsx": "react-jsx",
    "types": ["node", "jest", "@testing-library/jest-dom"],
  }
}
```

```javascript:jest.config.js
export default {
  preset: "ts-jest",
  testEnvironment: "jsdom",
  setupFilesAfterEnv: ["./jest.setup.ts"],
  transform: {
    "^.+\\.(ts|tsx)$": "ts-jest",
  },
  moduleNameMapper: {
    "\\.(css|less)$": "identity-obj-proxy"
  },
};
```

# 原因

Chakra UIを導入した際、`tsconfig.app.json`にパスのエイリアスを設定しています。

```json:tsconfig.app.json（一部省略）
{
  "compilerOptions": {
    // 省略
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  // 省略
}
```

`App.tsx`などでimportする時にエイリアスの`@`を使用してパスを記載しているのですが、それがうまく認識されていないようです。

# 解決方法

1. `tsconfig.json`に`paths`を追加
   ```diff_json
   {
     "files": [],
     "references": [
       { "path": "./tsconfig.app.json" },
       { "path": "./tsconfig.node.json" }
     ],
     "compilerOptions": {
       "jsx": "react-jsx",
       "types": ["node", "jest", "@testing-library/jest-dom"],
   +   "paths": {
   +     "@/*": ["./src/*"]
   +   }
     }
   }
   ```

2. `jest.config.js`の`moduleNameMapper`に追記
   ```diff_javascript:jest.config.js
   export default {
     preset: "ts-jest",
     testEnvironment: "jsdom",
     setupFilesAfterEnv: ["./jest.setup.ts"],
     transform: {
       "^.+\\.(ts|tsx)$": "ts-jest",
     },
     moduleNameMapper: {
       "\\.(css|less)$": "identity-obj-proxy",
   +   "^@/(.*)$": "<rootDir>/src/$1"
     },
   };
   ```

# おわりに

上記の修正で、無事テストが通るようになりました。

# 参考

https://github.com/ritsu21ctws/type-class