学生の頃、趣味で VBA や COBOL を書いていたのだが、仕事で書くようになったのは Java であった。Java で学んだことは今も血肉として活きているし、代えがたい愛着もある。Java の好きな点はいくつもあるが、とりわけ好きだったのが Enum である。
よくこんなコードを書いた。
public interface Options<K extends Serializable, V extends Serializable> {
K getKey();
V getValue();
static <K extends Serializable, ENUM extends Enum<ENUM> & Options<K,?>> ENUM getEnumFromKey(Class<ENUM> clazz, K key) {
for (ENUM enm : clazz.getEnumConstants())
if (key.equals(enm.getKey())) return enm;
throw new IllegalArgumentException();
}
static <V extends Serializable, ENUM extends Enum<ENUM> & Options<?,V>> ENUM getEnumFromValue(Class<ENUM> clazz, V value) {
for (ENUM enm : clazz.getEnumConstants())
if (value.equals(enm.getValue())) return enm;
throw new IllegalArgumentException();
}
}
public class Enums {
public enum Sex implements Options<Integer, String> {
FEMALE(0, "女性"),
MALE(1, "男性");
private final int key;
private final String value;
Sex(int key, String value) { this.key = key; this.value = value; }
@Override public Integer getKey() { return key; }
@Override public String getValue() { return value; }
}
}
class User {
@Getter
@Setter
private String name;
@Getter
private Enums.Sex sex;
@Getter
@Setter
private int age;
public void setSex(int sex) {
this.sex = Options.getEnumFromKey(Enums.Sex.class, sex);
}
}
上記コードは DB から取得した User Entity が、取得時点で int 定数を Enum に変換、カプセル化される様子を再現している。
Java の Enum は Singleton なので、データの一意性や型安全性、不変性を保証するときに強力な効果を発揮する。データ入力時点には意味も型安全も持たなかった int 定数は、意味も型安全も不変性も一意性も有している Enum で Field に保持されるので、それ以降の処理で定数やラベルの誤りなど発生しようがない。
Generics を使用することで、動的型付け言語と同じ型指定の柔軟性を持たせつつ、型の誤りは実装の段階で検出できる。これは動的型付け言語が逆立ちしても実現できない、型安全のためのチェック網だ。これはもう静的型付け言語による、型安全芸術と呼んでよろしかろう。
Java が魅せるオブジェクト指向と静的型付けの世界に耽溺していた私には、 当初 TypeScript に対してよい印象を持つことは難しかった。JavaScript との相互変換を意識するあまり、静的型付け言語としてはあまりに型安全性が脆弱な、”妥協的産物”というイメージがあったからである。例を挙げるなら Union 型だろう。
function getYearOfBirth(age: string|number) {
let numericAge
if (typeof age === "string") {
numericAge = Number(age)
if (Number.isNaN(numericAge)) {
throw Error()
}
} else {
numericAge = age
}
// 以降省略
この見苦しい関数は何だ。なぜ継承関係がない string 型と number 型の両方が、同じ関数の同じ引数に指定できてしまうのだ?言語仕様がリスコフの置換原則を否定しているではないか。これでは静的型付け以前に、オブジェクト指向言語と言えるのかすら怪しくはあるまいか。
今考えると、言語仕様を妥協したとしても、JavaScript と完全にトランスパイルできるようにしたのは、正しい判断だったと思っている。現実世界では、理想を主張する方が簡単だ。TypeScript を厳格な静的型付け言語に設計し、TypeScript から JavaScript へ変換できても、JavaScript のライブラリは TypeScript で使用不可能だったら、これほど広く使用される言語になったかどうか疑わしい。使われないものに価値はない。 Alt JS のデファクトスタンダード登場が遅れていたかもしれない。Microsoft 社の TypeScript 開発チームのこの英断こそ、プロの仕事と呼ぶに相応しい。
特に面白みもなく書いていた TypeScript が、私の中で輝きを放つきっかけとなったのが Literal 型であった。
type Fruit = "apple" | "orange" | "grape" | "banana"
Literal 型を初めて知ったときは驚愕した。まさか string や boolean といった型にまだ subtype を定義できる余地が残っていたとは思いもよらなかったからだ。Anders Hejlsberg の仕業だろうか。才能豊かな言語設計者の発想には頭が下がる。
Literal 型は Enum と似ている。似ているが 仰々しい Java の Enum より遙かにシンプルな Syntax だ。これはとんでもなくよいアイデアではあるまいか。
type user = {
name: string,
age: number
favoriteFruit: Fruit
...
}
これを Key Value 形式のデータ構造である Object 型に応用すれば、上述した Java の Enum と同じような役割を与えることが可能になるではないか。
TypeScript では Object の key を ` <T extends Object> keyof T ` 、value を ` <T extends Object> T[keyof T] `として型定義することができる。
export type Options<K extends number|string, V> = Readonly<Record<K, V>>
export type OptionsKey<T extends Options<any, any>> = keyof T
export type OptionsValue<T extends Options<any, any>> = T[keyof T]
/**
* Options の value から key を取得する
*
* @param options
* @param value
* @returns key
*/
export const getOptionsKeyFromValue = <T extends Options<any, any>, K extends keyof T, V extends T[keyof T]>(options: T, value: V): K =>
(Object.entries(options).find(el => el[1] === value) as [K, V])[0]
export const SEX = {
0: "女性",
1: "男性"
} as const satisfies Options<number, string>
export type User = {
name: string
sex: OptionsKey<typeof SEX>
age: number
}
これにより格納される値はただの int 定数であったとしても、コード上は意味が明確に定義された型安全のある別物となった。例えば、テストや Storybook の引数設定などで、Field 値が必要になったとしても、型だけ分かっていれば実値は必要ない。
const user: User = {
name: "test taro",
sex: getOptionsKeyFromValue(SEX, "男性"),
age: 23
}
この関数、さらに Literal 型の効力で選択肢が Suggest される。
何という便利、何という型安全性だろう。これこそ私が目指す型安全芸術だ。ただの JavaScript の Object が、TypeScript の型機能によって、別次元のものとなった。