コーディングエージェントにフォームを作ってもらい、少し修正を頼んだところ、次のエラーが出たとします。

Type 'number' is not assignable to type 'string'.

英語のエラーなので難しそうに見えますが、言っていることは単純です。「文字列を入れる場所へ、数値を入れようとしている」という意味です。

Next.jsのプロジェクトでは、.ts.tsxというファイルを頻繁に見かけます。そこで使われているのがTypeScriptです。自分ですべてを書けるようになる必要はありません。ただ、エージェントが変更した型と、エラーが指している場所を読めると、修正を丸投げせずに済みます。

TypeScriptの文法をすべて覚えることが目的ではありません。ここでは、Next.jsのコードを眺めたときに、値の形、関数の入口と出口、エージェントが型チェックを避けた箇所を読める範囲に絞ります。

TypeScriptはJavaScriptを検査する

JavaScriptでは、変数に文字列を入れたあとで数値を入れることもできます。ブラウザやサーバーがそのコードを実行するまで、間違いに気づけないことがあります。

function greet(name) {
  return `こんにちは、${name.toUpperCase()}さん`
}

greet("Noema") // 問題なし
greet(42)      // 実行するとエラー

JavaScriptでは、greet(42)を実行するまで、numbertoUpperCaseがない問題に気づけません。

TypeScriptでは、「この値は文字列」「この関数は数値を返す」といった情報をコードへ加えます。そして、実行前に値の使い方が合っているかを検査します。

function greet(name: string): string {
  return `こんにちは、${name.toUpperCase()}さん`
}

greet("Noema") // 問題なし
greet(42)      // 型エラー

name: stringは、引数のnameが文字列であることを表します。関数の後ろにある: stringは、この関数が文字列を返すことを表します。TypeScript側では、greet(42)を実行する前に型エラーとして示されます。

同じgreet関数をJavaScriptとTypeScriptで並べ、数値を渡した間違いにJavaScriptは実行後、TypeScriptは実行前に気づく違いを示す比較図

TypeScriptの型は、通常、ブラウザやNode.jsが実行するJavaScriptへ変換されると消えます。型が付いているから、不正な通信や入力を自動的に拒否できるわけではありません。外部から受け取ったデータは、実行時にも検証する必要があります。

.js.ts.jsx.tsx

Next.jsでは、次の拡張子を見かけます。

拡張子 中身
.js JavaScript
.ts TypeScript
.jsx JSXを含むJavaScript
.tsx JSXを含むTypeScript

JSXは、JavaScriptやTypeScriptの中に、HTMLに似た見た目で画面を書く仕組みです。

export function Welcome() {
  return <p>ようこそ</p>
}

Next.jsのpage.tsxやReactコンポーネントに.tsxが多いのは、画面とTypeScriptを同じファイルへ書くためです。App Routerやpage.tsxの役割は、シリーズ内の「AI開発で困らないためのNext.js」で説明しています。

まず値の型を見る

よく使う型は多くありません。

const title: string = "お知らせ"
const price: number = 1200
const published: boolean = true
const tags: string[] = ["AI", "Next.js"]
  • stringは文字列
  • numberは数値
  • booleantrueまたはfalse
  • string[]は文字列の配列

TypeScriptは、右側の値から型を推測できます。そのため、次の: stringは省略できます。

const title = "お知らせ"

これを型推論と呼びます。型が書かれていないからといって、型チェックされていないとは限りません。VS Codeで変数へカーソルを合わせると、推測された型を確認できます。

オブジェクトの形を読む

記事や利用者の情報は、複数の値をまとめたオブジェクトで表すことがよくあります。

stringやnumberという値の型がArticleオブジェクトの形にまとまり、ReactのCardPropsとして画面の部品へ渡る流れを示す図

type Article = {
  id: string
  title: string
  published: boolean
}

const article: Article = {
  id: "article-123",
  title: "はじめての記事",
  published: false,
}

Articleは、「idtitleは文字列、publishedは真偽値」という形に付けた名前です。

interfaceを使って、同じように書くこともできます。

interface Article {
  id: string
  title: string
  published: boolean
}

typeinterfaceには細かな違いがありますが、エージェントが書いた一般的なNext.jsコードを読む段階では、どちらも「オブジェクトの形に名前を付けている」と捉えて構いません。プロジェクト内で使い方が統一されているかを確認する方が大切です。

Reactのpropsにも型が付く

type ArticleCardProps = {
  title: string
  description?: string
}

export function ArticleCard({ title, description }: ArticleCardProps) {
  return (
    <article>
      <h2>{title}</h2>
      {description && <p>{description}</p>}
    </article>
  )
}

description??は、その値がなくてもよいことを表します。このコンポーネントではtitleは必須ですが、descriptionは省略できます。

エージェントが新しいpropsを追加したときは、型だけでなく、そのコンポーネントを使っている側も変更されているかを見ます。

「どちらか」を表すunion型

一つの値が複数の候補を取るときは、|を使います。

type Status = "draft" | "published" | "archived"

const status: Status = "draft"

このStatus"editing"を入れると型エラーになります。単なるstringではなく、許可する値を三つに絞っているからです。

function showMessage(value: string | null) {
  if (value === null) {
    return "値がありません"
  }

  return value.toUpperCase()
}

string | nullは、文字列またはnullです。文字列用の処理をする前に、nullではないことを確認しています。このように、条件分岐によって候補を絞ることをnarrowingと呼びます。

nullundefined

どちらも「値がない」場面で出てきますが、同じではありません。

  • undefined:値がまだ設定されていない、または項目が存在しない
  • null:値がないことを明示的に入れている

たとえば、データベースでは未設定の値をnullで返し、JavaScriptでは存在しないプロパティがundefinedになる、といった違いがあります。

type User = {
  name: string
  avatarUrl: string | null
}

この場合、avatarUrlという項目自体は必ずありますが、中身は文字列またはnullです。

type User = {
  name: string
  avatarUrl?: string
}

こちらは、avatarUrlという項目がない可能性があります。API、データベース、画面でこの扱いが揃っていないと、型エラーや表示不具合が起きます。

関数の入口と出口を確認する

関数を読むときは、処理を一行ずつ理解する前に、何を受け取り、何を返すのかを見ます。

関数getUserの入口にUserId型、出口にPromise Userまたはnull型があり、型を関数の契約として読む図

type CreateArticleInput = {
  title: string
  body: string
}

type Article = {
  id: string
  title: string
  body: string
}

function createArticle(input: CreateArticleInput): Article {
  return {
    id: crypto.randomUUID(),
    title: input.title,
    body: input.body,
  }
}

この関数はCreateArticleInputを受け取り、Articleを返します。型名が具体的なら、処理の役割も読みやすくなります。

エージェントが一つの巨大なData型をあらゆる場所で使っている場合は、「画面入力」「DBから取得した値」「APIの返答」が区別されているかを確認します。

asyncawaitPromise

API通信やデータベース処理は、結果が返るまで時間がかかります。そのため、非同期処理として書かれます。

async function getArticle(id: string): Promise<Article> {
  const response = await fetch(`/api/articles/${id}`)
  const article = await response.json()
  return article
}

asyncが付いた関数はPromiseを返します。Promise<Article>は、「処理が終わったらArticleが得られる」という意味です。awaitは、その結果を待ってから次へ進むために使います。

ここで注意したいのは、response.json()の中身が本当にArticleかどうかです。TypeScriptでArticleと書いただけでは、外部APIの返答を検証したことにはなりません。必要なら、受信時に項目や値を検証します。

anyunknown、型アサーション

型エラーを消すだけなら、anyを使えます。

const data: any = await response.json()

しかし、anyはその値に対する型チェックをほぼ止めます。エージェントが型エラーを直したと言いながらanyを追加していたら、問題を解決したのではなく、検査を外した可能性があります。

外部から来た正体不明の値には、unknownを使う方法があります。

const data: unknown = await response.json()

unknownのままでは、プロパティへ自由にアクセスできません。中身を確認してから使う必要があるため、anyより安全です。

次のas Articleは型アサーションです。

const article = data as Article

これは「TypeScriptには判定できないが、私はArticleだと分かっている」と伝える書き方です。データそのものを変換したり検証したりするものではありません。根拠のないasや、二重のas unknown asが増えていたら、エージェントへ理由を確認します。

型エラーは三つに分けて読む

長い型エラーも、最初は次の三点だけ見ます。

TypeScriptの型エラーを、値の型が違う、必須項目が足りない、nullの可能性がある、の三種類に分けて読む図

  1. どのファイルの何行目か
  2. 実際に渡した型は何か
  3. 期待されている型は何か
Argument of type 'number' is not assignable to parameter of type 'string'.

この場合、実際の値はnumber、受け取り側が期待しているのはstringです。

複雑なエラーでは、オブジェクトの中の一項目だけが違うことがあります。

Property 'email' is missing in type ...

これは、必要なemailが渡されていないという意味です。エラーメッセージ全体を理解しようとせず、missingnot assignableexpectedと、その近くにある型名から確認します。

エージェントの変更を確認する

TypeScriptの変更では、次を確認します。

  • 新しいデータに型が付いているか
  • API、データベース、画面で同じ項目名と型を使っているか
  • 必須項目へ勝手に?を付けていないか
  • anyを増やしていないか
  • 根拠のないasでエラーを隠していないか
  • nullundefinedの扱いが揃っているか
  • Promiseawaitし忘れていないか
  • 型チェックが成功しているか
  • 型チェック後に、実際の入力でも動作確認したか

エージェントには、次のように頼めます。

この変更で追加・変更した型を一覧にしてください。 API、データベース、Reactコンポーネントのどこで使われるかも対応させてください。

型エラーをanyや根拠のない型アサーションで隠さず、実際のデータの形に合わせて直してください。 外部から受け取る値は、TypeScriptの型だけでなく実行時の検証が必要か確認してください。

型チェックの結果と、実際のブラウザ操作で確認した結果を分けて報告してください。

TypeScriptは、すべての不具合を防ぐ仕組みではありません。それでも、値の形が合わない場所を実行前に見つけ、コードの入口と出口を読みやすくしてくれます。エージェントが型を追加したときは、型が厳しいか緩いかより、その型が実際のデータを正しく表しているかを確認してください。