TypeScriptディープリサーチ:仕組み・型システム・実務での使い方
1. TypeScriptとは
TypeScriptは、JavaScriptに静的な型チェックの仕組みを追加したプログラミング言語です。
TypeScriptで書いたコードは、最終的にJavaScriptへ変換されて実行されます。ブラウザやNode.jsがTypeScriptを直接実行するわけではありません。
TypeScript
↓ 型チェック・変換
JavaScript
↓
ブラウザ・Node.js
TypeScriptの公式サイトでは、TypeScriptを「JavaScriptを基盤として型と開発ツールを提供する言語」と位置づけています。(TypeScript)
2. 現在のTypeScript
2026年7月現在、npmで公開されている最新版はTypeScript 7.0.2です。(npm)
TypeScript 7では、コンパイラと言語サービスが従来のTypeScript実装からGoによるネイティブ実装へ移行しました。公式発表では、大規模プロジェクトのフルビルドにおいて、おおむね8~12倍の高速化が報告されています。(Microsoft Developer Blog)
ただし、Vueを使用する場合は注意が必要です。TypeScript公式は、TypeScript 7.0公開時点では、Vue・Svelte・Astro・MDXなどの言語サービス連携について、当面TypeScript 6.0環境を継続利用するよう案内しています。TypeScript 7.0には従来のコンパイラAPIがまだ含まれていないためです。(Microsoft Developer Blog)
したがって、Vue 3の学習環境では次の判断が現実的です。
TypeScript単体の学習
→ TypeScript 7でもよい
Vue 3・Volar・vue-tscを使用
→ 各ツールの対応状況を確認し、必要ならTypeScript 6系を使用
3. TypeScriptは「実行時の型チェック」ではない
TypeScriptの型は、原則としてコンパイル後に消えます。
const age: number = 20
変換後は次のようなJavaScriptになります。
const age = 20
つまり、外部APIやフォームから取得したデータが本当に期待した型かどうかを、TypeScriptだけで保証することはできません。
例えば、次の型定義があっても、
type User = {
id: number
name: string
}
APIが実際に返す値が次のようになっている可能性があります。
{
"id": "1",
"name": null
}
TypeScriptの型は実行時には存在しないため、外部データには別途バリデーションが必要です。
function isUser(value: unknown): value is User {
if (typeof value !== "object" || value === null) {
return false
}
const user = value as Record<string, unknown>
return (
typeof user.id === "number" &&
typeof user.name === "string"
)
}
ここはTypeScript学習で特に重要です。
TypeScriptは、コード上で想定した型の整合性を確認する。
外部データが正しいことまで自動的に保証するものではない。
4. TypeScriptの型システム
構造的型付け
TypeScriptは、型の名前ではなく、プロパティやメソッドの構造を比較して互換性を判断します。
interface User {
name: string
}
class Employee {
name = "田中"
department = "開発部"
}
const user: User = new Employee()
EmployeeがUserを明示的に継承していなくても、必要なnameプロパティを持っているため代入できます。
この仕組みを構造的部分型付けと呼びます。(TypeScript)
JavaやC#のように、型名や明示的な継承関係を重視する方式とは異なります。
型推論
TypeScriptでは、すべての変数へ型を書く必要はありません。
const name = "田中"
const count = 10
TypeScriptは次のように推論します。
name // string
count // number
明らかな型まで毎回書くと冗長になるため、実務では型推論を利用しつつ、関数の引数、外部境界、複雑なオブジェクトなどへ明示的に型を付けます。
Union型
複数の型のうち、いずれかを表します。
type Status = "idle" | "loading" | "success" | "error"
let status: Status = "loading"
文字列リテラルのUnion型を利用すると、任意の文字列ではなく、許可した値だけを扱えます。
status = "complete"
// エラー
Intersection型
複数の型を組み合わせます。
type Person = {
name: string
}
type Employee = {
employeeId: number
}
type Staff = Person & Employee
Staffには両方のプロパティが必要です。
const staff: Staff = {
name: "田中",
employeeId: 100
}
ただし、同じプロパティに矛盾する型を指定すると、交差結果がneverになる場合があります。
type A = {
id: string
}
type B = {
id: number
}
type C = A & B
// idはstring & numberとなり、実質never
Narrowing
Union型の値を条件分岐によって、より具体的な型へ絞り込む仕組みです。
function format(value: string | number): string {
if (typeof value === "number") {
return value.toFixed(2)
}
return value.toUpperCase()
}
typeof、instanceof、in、真偽値判定、独自の型ガードなどがNarrowingに利用されます。
TypeScriptは制御フローを解析し、それぞれの分岐で変数の型を絞り込みます。(TypeScript)
5. interfaceとtypeの正確な違い
共通点
interfaceとtypeは、どちらもTypeScriptで型を表現するために使います。
interface UserInterface {
id: number
name: string
}
type UserType = {
id: number
name: string
}
この例では、ほぼ同じ用途で使用できます。
ただし、厳密には次の違いがあります。
| 内容 | interface |
type |
|---|---|---|
| オブジェクト型の定義 | 可能 | 可能 |
extendsによる継承 |
可能 | 不可 |
&による型の合成 |
可能 | 可能 |
| 同名宣言 | 宣言マージされる | 重複エラー |
| Union型 | 直接は定義できない | 定義できる |
| プリミティブ型の別名 | 不可 | 可能 |
| タプル型 | 表現可能だが通常は不向き | 適している |
| Mapped Types | 宣言本体では使用不可 | 使用可能 |
| 条件型 | 不可 | 可能 |
継承
interface
interface Person {
name: string
}
interface Employee extends Person {
employeeId: number
}
type
typeではextendsによる継承ではなく、Intersection型で合成します。
type Person = {
name: string
}
type Employee = Person & {
employeeId: number
}
「typeは継承できない」という表現は概ね正しいですが、交差型を使って近い構造を作ることはできると補足するのが適切です。
継承時のプロパティ競合
interfaceでは、互換性のないプロパティを上書きするとエラーになります。
interface Base {
id: string
}
interface Derived extends Base {
id: number
}
一方、Intersection型ではプロパティごとの交差が計算されます。
type Base = {
id: string
}
type Derived = Base & {
id: number
}
この場合、idは次の型になります。
string & number
実質的にはneverです。
この違いにより、オブジェクト型を安全に拡張したい場合は、interface extendsの方がエラーを理解しやすいことがあります。
宣言マージ
interfaceは、同じ名前を複数回宣言すると内容が統合されます。
interface User {
id: number
}
interface User {
name: string
}
const user: User = {
id: 1,
name: "田中"
}
typeでは同名宣言できません。
type User = {
id: number
}
type User = {
name: string
}
// Duplicate identifier
宣言マージはライブラリの型拡張で便利ですが、意図せず型が拡張される可能性もあるため注意が必要です。
Mapped Types
Mapped Typesは、既存の型のキーを利用して新しい型を生成する機能です。
type User = {
id: number
name: string
}
type OptionalUser = {
[K in keyof User]?: User[K]
}
これはtypeで定義します。
interface OptionalUser {
[K in keyof User]?: User[K]
}
このように、interfaceの宣言本体へMapped Type構文を書くことはできません。
ただし、Mapped Typeで作った型をinterfaceが継承できる場合はあります。そのため、「interfaceではMapped Typesを一切利用できない」ではなく、interfaceの本体としてMapped Type構文を宣言できないと説明する方が正確です。
実務での使い分け
次の基準が分かりやすいです。
interfaceが向いている場面
interface User {
id: number
name: string
}
- オブジェクトの構造を定義する
- クラスが
implementsする契約を作る -
extendsで拡張する - 宣言マージを利用するライブラリ型を扱う
typeが向いている場面
type Status = "idle" | "loading" | "success"
type UserId = string | number
type Coordinate = [number, number]
- Union型
- Intersection型
- タプル型
- 関数型
- Mapped Types
- Conditional Types
- プリミティブ型の別名
単純なオブジェクト型では、プロジェクトのコーディング規約に合わせれば、どちらを利用しても問題ないことが多いです。
6. Generics
Genericsは、型を固定せず、利用時に型を受け取る仕組みです。
function identity<T>(value: T): T {
return value
}
const numberValue = identity<number>(10)
const stringValue = identity<string>("hello")
型推論が働くため、通常は型引数を省略できます。
const value = identity("hello")
// string
Genericsを使うことで、型安全性を維持しながら再利用可能な関数やコンポーネントを作れます。(TypeScript)
Vueでは、APIレスポンスや共通コンポーネントなどで利用できます。
type ApiResponse<T> = {
data: T
status: number
message: string
}
type User = {
id: number
name: string
}
const response: ApiResponse<User[]> = {
data: [
{
id: 1,
name: "田中"
}
],
status: 200,
message: "success"
}
7. Utility Types
TypeScriptには、既存の型を変換するためのUtility Typesが用意されています。(TypeScript)
Partial
すべてのプロパティを任意にします。
type User = {
id: number
name: string
}
type UserUpdate = Partial<User>
Required
すべてのプロパティを必須にします。
type RequiredUser = Required<User>
Pick
必要なプロパティだけを取得します。
type UserName = Pick<User, "name">
Omit
指定したプロパティを除外します。
type UserWithoutId = Omit<User, "id">
Record
キーと値の型を定義します。
type Role = "admin" | "user" | "guest"
const permissions: Record<Role, string[]> = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"]
}
Awaited
Promiseの中身の型を取り出します。
type Result = Awaited<Promise<string>>
// string
8. any・unknown・never
any
型チェックを実質的に無効化します。
let value: any = "hello"
value.notExist.method()
コンパイルエラーになりません。
既存JavaScriptから段階的に移行する場合などを除き、安易なanyの使用は避ける方が安全です。
unknown
どのような値か分からないことを表します。
let value: unknown = "hello"
そのままでは操作できません。
if (typeof value === "string") {
console.log(value.toUpperCase())
}
外部API、例外、JSONなど、信頼できない値にはunknownが適しています。
never
起こり得ない値を表します。
function throwError(message: string): never {
throw new Error(message)
}
網羅性チェックにも利用できます。
type Status = "success" | "error"
function handleStatus(status: Status): string {
switch (status) {
case "success":
return "成功"
case "error":
return "失敗"
default: {
const exhaustiveCheck: never = status
return exhaustiveCheck
}
}
}
Union型へ新しい値を追加した際、未処理の分岐を検出できます。
9. 型アサーション
型アサーションは、開発者がTypeScriptへ型を伝える機能です。
const input = document.querySelector("#name") as HTMLInputElement
ただし、型アサーションは実行時チェックではありません。
const value = "hello" as unknown as number
このようなコードも書けてしまうため、型安全性を損なう可能性があります。
可能な限り、型推論、型ガード、Genericsなどで解決し、型アサーションは必要な範囲に限定します。
10. satisfies
satisfiesは、値が指定した型を満たしているか確認しながら、値本来の詳細な型推論を維持する構文です。
type RouteName = "home" | "users"
const routes = {
home: "/",
users: "/users"
} satisfies Record<RouteName, string>
型注釈との違いは、オブジェクトの各値について、より具体的な推論を残しやすい点です。
設定オブジェクト、ルート定義、定数マップなどで有効です。
11. tsconfig.jsonで重要な設定
TypeScriptの安全性は、コードだけでなくtsconfig.jsonの設定にも大きく左右されます。
学習・新規開発では、まずstrictを有効にするのが基本です。
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
}
}
strict
複数の厳格な型チェックをまとめて有効にします。
"strict": true
noUncheckedIndexedAccess
配列やオブジェクトをキーで取得した結果に、undefinedの可能性を追加します。
const users = ["田中"]
const firstUser = users[1]
// string | undefined
実行時に存在しない要素へアクセスする問題を検出しやすくなります。
exactOptionalPropertyTypes
次の?を、単純な「値がundefinedでもよい」ではなく、プロパティ自体を省略できるという意味に厳密化します。
type User = {
name?: string
}
この設定を有効にすると、nameを省略することと、name: undefinedを明示することを区別できます。(TypeScript)
12. TypeScriptとESLintの違い
TypeScriptコンパイラとESLintは役割が異なります。
| ツール | 主な役割 |
|---|---|
| TypeScript | 型の整合性を確認する |
| ESLint | コード品質や記述ルールを確認する |
| Prettier | コードの見た目を整形する |
TypeScriptのエラー例:
const age: number = "20"
ESLintが検出する例:
const unusedValue = 10
TypeScript対応ESLintでは、型情報を利用したルールも使用できます。現在のtypescript-eslintは、TypeScriptコードをESLintで解析するための公式ツール群です。(npm)
13. Vue 3で特に重要なTypeScript
Props
<script setup lang="ts">
type Props = {
title: string
count?: number
}
const props = defineProps<Props>()
</script>
親から子へ渡される値の型を定義します。
Emit
<script setup lang="ts">
const emit = defineEmits<{
save: [id: number]
cancel: []
}>()
emit("save", 1)
emit("cancel")
</script>
子から親へ通知するイベント名と引数を型安全に定義します。
ref
const count = ref<number>(0)
初期値から推論できる場合は型引数を省略できます。
const count = ref(0)
初期値がnullの場合は明示的な型が必要になることがあります。
const user = ref<User | null>(null)
computed
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
戻り値は通常、自動で推論されます。
InjectionKey
import type { InjectionKey, Ref } from "vue"
export const countKey: InjectionKey<Ref<number>> =
Symbol("count-key")
provideとinjectで同じ値の型を共有できます。
provide(countKey, count)
const injectedCount = inject(countKey)
14. TypeScriptでよくある誤解
「TypeScriptならバグがなくなる」
なくなりません。
TypeScriptが検出できるのは、主に型として表現された不整合です。
次のような問題は別途対応が必要です。
- 業務ロジックの誤り
- APIの実データ不正
- SQLやバックエンドの問題
- セキュリティ上の問題
- 非同期処理の競合
- UIやアクセシビリティの問題
「型をたくさん書くほどよい」
必ずしもそうではありません。
const count: number = 1
このnumberは推論できるため、省略しても問題ありません。
const count = 1
型注釈は、読み手やコンパイラに必要な箇所へ付けます。
「interfaceはオブジェクト、typeはそれ以外」
完全には正しくありません。
typeでもオブジェクト型を定義できます。
type User = {
id: number
}
より正確には、
-
interfaceはオブジェクト構造の宣言と拡張に適している -
typeは型エイリアスとして、より幅広い型表現ができる
という違いです。
「TypeScriptはJavaScriptと別言語なので、JavaScriptを知らなくてもよい」
TypeScriptはJavaScriptを基盤としています。
非同期処理、クロージャ、配列操作、オブジェクト、モジュール、DOM、PromiseなどのJavaScript知識が前提です。
TypeScriptを理解するには、まずJavaScriptの実行時の挙動を理解する必要があります。
15. 学習優先順位
Vue 3で実務対応を目指す場合は、次の順番が効率的です。
第1段階:基礎
- プリミティブ型
- 配列・オブジェクト・関数の型
- 型推論
- Union型
-
typeとinterface - オプショナルプロパティ
第2段階:実装で頻出
- Narrowing
- Generics
- Utility Types
unknown- 型ガード
-
keyofとtypeof satisfies
第3段階:Vue 3
-
refとreactive - Props
- Emit
computedwatch- DOM参照
- APIレスポンス
- Provide/Injectと
InjectionKey
第4段階:高度な型
- Mapped Types
- Conditional Types
- Indexed Access Types
- Template Literal Types
- Discriminated Union
- 型レベルの再利用
TypeScript公式ドキュメントも、基礎、日常的な型、Narrowing、関数、オブジェクト型、Generics、型操作という順で構成されています。(TypeScript)
まとめ
TypeScriptの本質は、単に変数へstringやnumberを書くことではありません。
重要なのは次の点です。
- JavaScriptに静的型チェックを追加する言語である
- 型は原則として実行時には消える
- 構造的型付けと型推論を採用している
- Narrowingによって安全にUnion型を扱える
-
interfaceとtypeは重なる部分が多いが、拡張方法や表現力が異なる - GenericsやUtility Typesにより再利用可能な型を作れる
- 外部データには実行時バリデーションが必要
-
strictなどのコンパイラ設定が型安全性を左右する - VueではProps、Emit、
ref、APIレスポンス、InjectionKeyの型付けが特に重要
また、現在のTypeScript 7は大幅な高速化を実現していますが、Vueを含む一部のフレームワーク連携ではTypeScript 6系が必要になる可能性があります。学習環境では、単に最新版へ上げるのではなく、Vite、Vue、Volar、vue-tscとの対応状況を確認することが重要です。(Microsoft Developer Blog)