フロントエンドエンジニアの友人と“型”で話がすれ違った原因

※ 本記事は、技術書典で配布している『**[ゆめみ大技林 '23](https://techbookfest.org/product/pTZvmVyZJG2Mjyqs9SUWdd?productVariantID=tqRci9Rc5Qri99GHd9UjEa)**』に収録されています。他のメンバーの記事もぜひご覧ください！

# はじめに

Android アプリエンジニアのどぎーです。
趣味でフロントエンドエンジニアの友人と開発する機会があり、その際に**型という言葉の解釈で話が噛み合っていない**ことに気付きました。
友人が扱っている TypeScript も、私が扱っている Kotlin も、静的型付け言語で型という概念は共通してあるはずです。

ではなぜ型という言葉の解釈で話のすれ違いが生じたのか…

その理由は、**TypeScript と Kotlin の型システムが異なっている**という点にありました。TypeScript の型システムを**構造的部分型**、Kotlin の型システムを**公称型**といいます。
本章では UI の実装例を題材としながら、それらの型システムの違いを学んでいきます。

# 対象者

- 普段の開発で公称型の言語（Kotlin など）を扱っていて、これから構造的部分型の言語（TypeScript など）を学ぼうとしている方
- その逆の方
- 「はじめに」を読んで少しでも興味をそそられた方

私は TypeScript を学ぶのにすごく苦労しました。
というのも、長い間 TypeScript と Kotlin の型システムが同じものだと思い込んでいたためです。
ただある時、それらの**型システムが異なることを知って、急に霧が晴れる**ような感覚がありました。
私の場合は普段の開発で公称型を扱っていて、構造的部分型を受け入れるのに苦労しましたが、その逆のパターンで苦労している方もいるはずです。
そんな皆さんにとって、本章が少しでも学びの助けになれば幸いです。

それでは早速本題に入っていきましょう！

# 公称型と構造的部分型

公称型と構造的部分型は、いずれも型システムの種類です。
これらは具体的にどのようなものなので、どのような違いがあるのでしょうか。
次にまとめてみました。

- **公称型**
    - **置換できない**
    - 名前によって区別される
    - 厳密さ・安全性
- **構造的部分型**
    - **構造が同じ場合に置換できる**
    - 構造によって区別される
    - 柔軟性・拡張性

ここでは「置換できるか・できないか」という違いに着目してみましょう。

## 公称型（Kotlin）の場合

Kotlin で次の２つのクラスを定義してみます。

```kotlin:Dog.kt
class Dog(
  val name: String,
) {
  fun move() {
    // わんわんは動く
  }
}
```

```kotlin:Cat.kt
class Cat(
  val name: String,
) {
  fun move() {
    // にゃんにゃんも動く
  }
}
```

`Dog` クラスと `Cat` クラスはどちらも、`name` プロパティと `move` メソッドを持っています。
クラスの構造は同じですが、Kotlin は公称型です。
公称型では、クラスはその名前で区別されるので、`Dog` クラスと `Cat` クラスは置換できません。
すなわち次のコードはコンパイルエラーを起こします。
右辺のインスタンスの型が、左辺の変数の型と異なるというエラーです。

```kotlin
val tama: Dog = Cat("tama")
// -> Type mismatch: inferred type is Cat but Dog was expected

val taro: Cat = Dog("taro")
// -> Type mismatch: inferred type is Dog but Cat was expected
```

## 構造的部分型（TypeScript）の場合

TypeScript でも `Dog` クラスと `Cat` クラスを定義してみます。

```ts:Dog.ts
class Dog {
  name: string;
 
  constructor(name: string) {
    this.name = name;
  }

  move() {
    // わんわんは動く
  }
}
```

```ts:Cat.ts
class Cat {
  name: string;
 
  constructor(name: string) {
    this.name = name;
  }

  move() {
    // にゃんにゃんも動く
  }
}
```

構造的部分型においては、クラスはその構造によって区別されます。
`Dog` クラスと `Cat` クラスはどちらも `name` プロパティと `move` メソッドを持っており、同じ構造です。
公称型の言語に慣れ親しんでいる私にとっては大変驚きですが、TypeScript では次のコードが実際に動きます。
**`Dog` 型の変数に `Cat` 型のインスタンスが代入できてしまう**のです。

```ts
const dog: Dog = new Cat("tama")
console.log(dog.name)
// => "tama"
```

以上の解説で、同じ静的型付け言語でも異なる２つの型システムが存在することを学びました。
次節からは UI の実装例を題材としながら、
Android アプリエンジニアの私とフロントエンドエンジニアの友人の話がどういった点で食い違ってしまったのかを紹介していきます。

# UI の実装

本章で実装する UI の要件は次のとおりです。

- ３種類のコンポーネントをリスト表示する
    - 横長のリストアイテム `ListItem`
    - 大きい正方形のリストアイテム `LargeListItem`
    - 複数カラムのリストアイテム `MultiColumnListItem`
- 各コンポーネントの個数に制限はない
- コンポーネントの順番は好きなように入れ替えられる

実際に実装したスクショは次のとおりです。

| Jetpack Compose | React Native |
|:--:|:--:|
| <img alt="Jetpack Compose での実装例" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933349/64dc64e3-721a-a595-f09e-bc43de849aba.png" width="300px" /> | <img alt="React Native での実装例" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933349/54199537-6efa-4e66-0166-c1326fac233f.png" width="300px" /> |


次節からは実際に UI を実装していきます。
ただしページ数の制約のため、掲載しているサンプルコードの一部を簡略化しています。
そのままでは動かないコードもあります。
章末に掲載した GitHub のリポジトリにサンプルコードの全体を載せてありますので、興味のある方はぜひそちらをご確認ください！

## Jetpack Compose（Kotlin）での実装

Jetpack Compose でリスト表示を実現するには Lazy リストを使用します。
今回は垂直方向のリストのため `LazyColumn` を使用します。
`LazyColumn` の実装例は次のとおりです。

```kotlin
// 出典：https://developer.android.com/jetpack/compose/lists

import androidx.compose.foundation.lazy.items

@Composable
fun MessageList(messages: List<Message>) {
    LazyColumn {
        items(messages) { message ->
            MessageRow(message)
        }
    }
}
```

`LazyColumn` のブロックで `items` メソッドを実行しています。
`items` メソッドの `items` という引数にリストを渡し、`items` メソッドのブロックの中で１つずつ取り出すことができます。
今回の要件では、３種類のリストアイテムを好きな個数、好きな順番で並べることが求められています。
`items` に渡せる型は１種類なので、どうにか工夫して３種類のリストアイテムを表現する必要があります。
こんなときに便利なのが Kotlin の `sealed class` です。
`sealed class` で３種類のリストアイテムを表現すると次のようになります。

```kotlin:HomeScreenUiModel.kt
sealed class HomeScreenUiModel {

  // 横長のリストアイテム
  data class ListItemUiModel(
    val text: String,
  ) : HomeScreenUiModel()

  // 大きい正方形のリストアイテム
  data class LargeListItemUiModel(
    val text: String,
  ) : HomeScreenUiModel()

  // 複数カラムのリストアイテム
  data class MultiColumnListItemUiModel(
    val texts: List<String>,
  ) : HomeScreenUiModel()
}
```

`ListItemUiModel`, `LargeListItemUiModel`, `MultiColumnListItemUiModel` はいずれも `HomeScreenUiModel`
を継承しているので、`HomeScreenUiModel` として扱うことができます。
また `sealed` というキーワードを付けているので、これら３種類のクラス以外は `HomeScreenUiModel` を継承できません。
その結果、`HomeScreenUiModel` をそれら３種類の型で網羅的に条件分岐できます。
実際に `HomeScreenUiModel` を型で条件分岐し、`LazyColumn` で表示するコードは次のようになります。

```kotlin:HomeScreen.kt
@Composable
fun HomeScreen(uiModels: List<HomeScreenUiModel>) {
  LazyColumn {
    items(items = uiModels) { uiModel ->
      when (uiModel) {
        is HomeScreenUiModel.ListItemUiModel -> ListItem(text = uiModel.text)
        is HomeScreenUiModel.LargeListItemUiModel -> LargeListItem(text = uiModel.text)
        is HomeScreenUiModel.MultiColumnListItemUiModel -> MultiColumnListItem(texts = uiModel.texts)
      }
    }
  }
}
```

以上で、要件を満たす UI を実装できました！

## React Native（TypeScript）での実装

React Native でリスト表示を実現するには `FlatList` を使用します。
`FlatList` の実装例は次のとおりです。

```tsx
// 出典：https://reactnative.dev/docs/flatlist

const App = () => {
  return (
    <SafeAreaView style={styles.container}>
      <FlatList
        data={DATA}
        renderItem={({item}) => <Item title={item.title} />}
        keyExtractor={item => item.id}
      />
    </SafeAreaView>
  );
};
```

`data` という Props に表示したい内容のリストを渡し、`renderItem` に渡したコンポーネントで１つずつ表示していきます。
`data` に渡せる型は１種類なので、Jetpack Compose の場合と同様に、どうにか工夫して３種類のリストアイテムを表現する必要があります。
Kotlin ではクラスを使用しましたが、TypeScript ではユニオン型を使用します。
ユニオン型は型をバーティカルバー（`|`）で連結し、その連結した型のうちのどれかを表します。
ユニオン型で `HomeScreenUiModel` を表現すると次のようになります。

```ts:HomeScreenUiModel.ts
// 横長のリストアイテム
type ListItemUiModel = {
  type: 'item';
  text: string;
};

// 大きい正方形のリストアイテム
type LargeListItemUiModel = {
  type: 'large';
  text: string;
};

// 複数カラムのリストアイテム
type MultiColumnListItemUiModel = {
  type: 'multi-column';
  texts: string[];
};

type HomeScreenUiModel =
  | ListItemUiModel
  | LargeListItemUiModel
  | MultiColumnListItemUiModel;
```

`HomeScreenUiModel` は `ListItemUiModel`, `LargeListItemUiModel`, `MultiColumnListItemUiModel` からなるユニオン型です。
すなわち、`HomeScreenUiModel` はそれら３種類の型のうちどれかを表すこととなり、網羅的に条件分岐できます。
`FlatList` は `renderItem` に渡せるコンポーネントが１種類のため、その１種類のコンポーネントが、実際に表示する３種類のコンポーネントに分岐するよう実装してみます。
`renderItem` に渡すコンポーネント `HomeScreenUiModelBinder` の実装例は次のとおりです。

```tsx:HomeScreenUiModelBinder.tsx
const HomeScreenUiModelBinder: React.FC<Props> = ({item}) => {
  switch (item.type) {
    case 'item':
      return <ListItem text={item.text} />;
    case 'large':
      return <LargeListItem text={item.text} />;
    case 'multi-column':
      return <MultiColumnListItem texts={item.texts} />;
  }
};
```

`HomeScreenUiModel` を構成する３種類の `UiModel` は共通して `type` プロパティを持っているので、`type` プロパティの型は `item`, `large`
, `multi-column` という３種類の文字列からなるユニオン型になります。
したがって、`HomeScreenUiModel` がもつ `type` プロパティの値によって網羅的に条件分岐できています。
`HomeScreenUiModelBinder` を `FlatList` に渡し、`data` の内容を実際にリスト表示すると次のようになります。

```tsx:HomeScreen.tsx
const HomeScreen: React.FC<Props> = ({uiModels}) => (
  <FlatList<HomeScreenUiModel>
    data={uiModels}
    renderItem={item => <HomeScreenUiModelBinder item={item.item} />}
  />
);
```

以上で、要件を満たす UI を実装できました！

ここまで公称型における型定義と、構造的部分型における型定義を見てきました。
それぞれの雰囲気をなんとなく掴めたでしょうか。
どちらの例でも型という言葉を使用していましたが、その意味は異なっていました。
すなわち、

- 公称型：**型の名前そのものを定義する**
- 構造的部分型：**型の構造を定義する**

という違いがありました。
この違いが、Android エンジニアである私とフロントエンドエンジニアの友人との間で生まれた齟齬の原因となっていました。そして、私が TypeScript を学ぶ上での１つのハードルにもなっていたのです。

# まとめ

最後に本章のまとめです。

1. **同じ静的型付け言語でも、異なる型システムがある**
    1. 公称型：**構造が同じでも置換できない**
    1. 構造的部分型：**構造が同じ場合に置換できる**
1. 型定義
    1. フロントエンドエンジニアの友人：**型の構造を定義する**
    1. Android エンジニアの私：**型の名前そのものを定義する**

# おわりに

最後まで読んでいただきありがとうございました！
技術書の執筆は今回が初めてで、とてもよい経験となりました。
ぜひ来年も挑戦させていただきたいです…！
各種 SNS のアカウントを載せておきますので、ご質問やアドバイスがあれば、ぜひぜひお気軽にご連絡ください。

- GitHub: [@Kaito-Dogi](https://github.com/Kaito-Dogi)
- Twitter: [＠Kaito_Dogi](https://twitter.com/Kaito_Dogi)
- note: [kaito_dogi](https://note.com/kaito_dogi)

## サンプルコード

UI の実装例を次のリポジトリに置いています。
興味があればぜひご覧ください！

```url
https://github.com/Kaito-Dogi/type-systems
```

## 参考文献

1. [構造的部分型（structural subtyping）| TypeScript 入門『サバイバル
   TypeScript』](https://typescriptbook.jp/reference/values-types-variables/structural-subtyping)
1. [Lists and grids | Compose | Android Developers](https://developer.android.com/jetpack/compose/lists)
1. [Sealed classes and interfaces | Kotlin Documentation](https://kotlinlang.org/docs/sealed-classes.html)
1. [FlatList · React Native](https://reactnative.dev/docs/flatlist)
1. [TypeScript: Handbook - Unions and Intersection](https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html)
1. [ユニオン型（union type）| TypeScript入門『サバイバル TypeScript』
   Types](https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html)
1. [Keishin Yokomaku,（2023）, Kotlin で Either したい](https://speakerdeck.com/keithyokoma/either-in-kotlin)