🅰️

名前的型付けと構造的部分型について(TypeScriptとPHPを参考に)

に公開

構造的部分型について

TypeScriptは、構造的部分型の言語と言われます。

参考: https://typescriptbook.jp/reference/values-types-variables/structural-subtyping#構造的部分型

構造的部分型(structural typing) と呼ばれる型システムのことで、 名前的型付け(nominal typing) という型システムと2つの種類があります。

何者なのか

プログラミング言語の型システムは、「2つの型がいつ互換性があると見なされるか」というルールを定めています。

名前的型付け(Nominal Typing)

この方式では、型の互換性は「名前」と「継承関係」によって判断されます。

サンプル

名前的型付け言語のPHPで書きます。

以下は型が異なるのでエラーになります。

class Person {
    public string $name = '';
}

class User {
    public string $name = '';
}

Person $person = new Person();
User $user = new User();
$person = $user; // この場合エラーになります

構造的部分型(Structural Typing)

型の構造(メソッドやプロパティの形)に基づいて互換性を判断します。

サンプル

以下のTypeScriptのコードはエラーになりません。

class Person {
  public name: string = ''
}

class User {
  public name: string = ''
}

let user: User = new Person(); // 構造が同じため許可される

メリデメ

構造のメリットとして、柔軟性が高いことが挙げられます。

// インターフェースを明示的に実装していなくても、
// 構造が一致すれば使用可能
interface Logger {
  log: (message: string) => void
}

const consoleLogger = {
  log: (message: string) => console.log(message)
}

const fileLogger = {
  log: (message: string) => { /* ファイルに書き込む */ },
  additionalMethod: () => { /* 追加の機能 */ }
}

// 両方ともLoggerとして使用可能
const logMessage = (logger: Logger) => {
  logger.log("Hello")
}

logMessage(consoleLogger)
logMessage(fileLogger)

この場合、consoleLoggerは同一構造なので問題ないのは当然ですが、fileLoggerもadditionalMethodがあったとしてもエラーにならないのです。

逆にこの柔軟性が高いことがデメリットになることもあります。

interface Point2D {
  x: number
  y: number
}

interface Timestamp {
  x: number  // unix timestamp
  y: number  // ナノ秒
}

// 構造が同じため、互換性があると判断される
const point: Point2D = { x: 1, y: 2 }
const timestamp: Timestamp = point  // エラーにならない

このような場合、構造は同じですが内容は異なるものになる可能性もあります。
その場合でもエラーとならないため注意が必要です。

そこでTypeScriptでは、Branded Typeを使います。

Branded Types

Branded Typesは、既存の型に独自のタグを追加することで、同じ基底型から異なる型を作成する仕組みです。

type Brand<B> = { __brand: B }
type Branded<T, B> = T & Brand<B>

// 使用例
type UserId = Branded<string, 'UserId'>
type ProductId = Branded<string, 'ProductId'>

このようにプリミティブな型に対して、Brandの型を付与することでユニークなものにすることができます。

こうすることで、構造的部分型のTypeScriptを名前的型付けのように扱うことができます。
これを使うことで、以下のような厳密な型によるバリデーションが行えるようになります。

// 基本的なBrand型の定義
type Brand<K, T> = K & { readonly __brand: T }

type ValidEmail = Brand<string, 'ValidEmail'>
type ValidAge = Brand<number, 'ValidAge'>

interface StrictUserData {
  id: string
  email: ValidEmail
  name: string
  age: ValidAge
  preferences: {
    theme: 'light' | 'dark'
    notifications: boolean
  }
}

// ヘルパー関数
const createValidEmail = (email: string): ValidEmail | null => {
  return email.includes('@') ? email as ValidEmail : null
}

const createValidAge = (age: number): ValidAge | null => {
  return (age >= 18 && age <= 120) ? age as ValidAge : null
}

// より厳格な型チェック
const createStrictUser = (data: {
  id: string
  email: string
  name: string
  age: number
  preferences: {
    theme: 'light' | 'dark'
    notifications: boolean
  }
}): StrictUserData | null => {
  const validEmail = createValidEmail(data.email)
  const validAge = createValidAge(data.age)

  if (!validEmail || !validAge) return null

  return {
    ...data,
    email: validEmail,
    age: validAge
  } as StrictUserData
}

しかし、型安全にしすぎるあまり、全てをBrandedTypesにするのではなく、必要なものだけにするというバランスをとることが重要です。

構造的部分型の言語

最後に、名前的型付けの言語と構造的部分型の言語を主要なものを紹介して終わります。

言語 型システム
TypeScript 構造的部分型
Go 構造的部分型
OCaml 構造的部分型
Elm 構造的部分型
PureScript 構造的部分型
Java 名前的型付け
C# 名前的型付け
Swift 名前的型付け
Rust 名前的型付け
Kotlin 名前的型付け
PHP 名前的型付け
Objective-C 名前的型付け
Delphi 名前的型付け

参考

https://effect.website/docs/code-style/branded-types/
https://zenn.dev/okunokentaro/articles/01gmpkp9gzfyr1za5wvrxt0vy6
https://zenn.dev/nabee/articles/0dcfc36066230c

Discussion