TypeORMの導入でつまづいたこと

2023.12.05

はじめに

TypeORMを使ったWebアプリを一から作ろうとしたところ、何度かエラーでつまづいたところがありましたので備忘録として記事に残しておきます。

前提事項

環境は以下の通りです。

  • Node.js 18.16.0

必要なモジュールは公式のインストール手順に沿ってインストールされているものとします。package.jsonの内容は以下の通りです。

"devDependencies": {
    "@types/node": "^20.9.3",
    "@typescript-eslint/eslint-plugin": "^6.13.1",
    "@typescript-eslint/parser": "^6.13.1",
    "eslint": "^8.54.0",
    "eslint-config-prettier": "^9.0.0",
    "eslint-plugin-prettier": "^5.0.1",
    "prettier": "3.1.0",
    "ts-node": "^10.9.1",
    "typescript": "^5.3.2"
  },
  "dependencies": {
    "pg": "^8.11.3",
    "reflect-metadata": "^0.1.13",
    "typeorm": "^0.3.17"
  }

またフォルダ構成は以下の通りです。

app
├─ node_modules
├─ src
│ ├─ entity
│ ├─ migration
│ ├─ data-source.ts
│ └─ index.ts
├─ package.json
└─ tsconfig.json

エンティティを作成したら、The runtime will invoke the decorator with 2 arguments, but the decorator expects 1. エラー

entityフォルダにUser.tsを作成します。公式サイトのエンティティの例をそのまま記述します。

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity()
export class User {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    firstName: string

    @Column()
    lastName: string

    @Column()
    isActive: boolean
}

すると、以下のようにエラーになってしまいました。

Unable to resolve signature of class decorator when called as an expression.
The runtime will invoke the decorator with 2 arguments, but the decorator expects 1.

インストール手順には、TypeScriptの4.5以上を使うことと、tsconfig.jsonに以下の設定を追加しすることが書かれています。

ちなみに、experimentalDecoratorsは実験的なデコレーターを使えるようにするためのもので、emitDecoratorMetadataはデコレーターのメタデータが生成されるようにするためのものです。

参考:Documentation - Decorators

"experimentalDecorators": true,
"emitDecoratorMetadata": true,

上記を確認してもエラーが解消されなかったため、更に調べていくと、tsconfig.jsonのコンパイル対象の設定が誤っていました。

tsconfig.jsonは以下のようになっていました。

{
  "compilerOptions": {
    "target": "es2022",
    "module": "commonjs",
    "lib": ["es2022"],
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "moduleResolution": "node",
    "baseUrl": "src",
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/*"],
  "exclude": ["dist", "node_modules"],
  "compileOnSave": false
}

includeを見ると、srcフォルダ配下をワイルドカードで指定していますが、src/entityフォルダのように階層になっているフォルダが対象になっていませんでした。

そこで、includeを以下のように変更します。

"include": ["src/**/*"],

または、filesが指定されていなければデフォルトで**/*が対象になるので、includeごと削除します。

これでデコレーターに関するエラーは解消しました。

エンティティのプロパティで、typeorm Property 'id' has no initializer and is not definitely assigned in the constructor. エラー

デコレーターに関するエラーは解消しましたが、別の箇所でエラーになりました。

Property 'id' has no initializer and is not definitely assigned in the constructor.

クラスのプロパティがコンストラクタに設定されていないときに発生するエラーです。

このエラーはstrictPropertyInitializationを有効にしているときに発生します。今回の設定だと、strictを有効にしているため自動的にこの設定も有効になっています。

strictPropertyInitializationを無効にすればエラーは解消されますが、エンティティ以外のクラスにも影響してしまいます。

そこで、以下のように!または?をつけることで、コンストラクタに設定されていなくても問題ないことをTypeScriptに伝えます。!を使えば、必ず初期化されることを伝えます。?を使えば、undefinedの可能性があることを伝えます。どちらを使うかは、DBの値がnullになる可能性があるかどうかで判断します。

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity()
export class User {
    @PrimaryGeneratedColumn()
    id!: number

    @Column()
    firstName?: string

    @Column()
    lastName?: string

    @Column()
    isActive!: boolean
}

これでコンパイルエラーはなくなりました。

接続しようとして、Driver not Connected エラー

試しにDBに接続してみようと、index.tsを以下のようにしました。

import { AppDataSource } from './data-source'

const test = async () => {
  const result = await AppDataSource.manager.query("SELECT 'hoge'")
  console.log(result)
}
void test()

インポートしているdata-source.tsは以下のような設定です。

import 'reflect-metadata'
import { DataSource } from 'typeorm'

export const AppDataSource = new DataSource({
  type: 'postgres',
  host: 'localhost',
  port: 5434,
  username: 'fc_test',
  password: 'fc_test',
  database: 'fc_test',
  entities: ['src/entity/*.ts'],
  migrations: ['src/migration/*.ts'],
  logging: true,
  logger: 'file',
})

AppDataSource.initialize()
  .then(() => {
    console.log('Data Source has been initialized!')
  })
  .catch((err) => {
    console.error('Error during Data Source initialization', err)
  })

これで動かしてみると、エラーになってしまいました。

TypeORMError: Driver not Connected

よく見ると、DataSourceのinitializeメソッドはPromise型でした。DBにクエリを投げる前にDataSourceが初期化されていなかったようです。

そこで、クエリ前に初期化されるようにinitializeメソッドの位置を変更しました。

import { AppDataSource } from './data-source'

const test = async () => {
  await AppDataSource.initialize()
    .then(() => {
      console.log('Data Source has been initialized!')
    })
    .catch((err) => {
      console.error('Error during Data Source initialization', err)
    })

  const result = await AppDataSource.manager.query("SELECT 'hoge'")
  console.log(result)
}
void test()

無事にDBに接続することができました。

おわりに

普段プロジェクトで何気なく触っているものでも、改めて一から作ってみると戸惑うことがたくさんあり、勉強になりました。

この記事がどなたかのお役に立てれば幸いです。