TypeScript

TypeScript で型情報を実行時に利用できる型定義の方法

TypeScript ではコンパイル時に型情報が落ちるため、実行時に interface の key などの型情報を利用することはできません。

大抵の場合は問題無いのですが、たまに型情報を利用したくなるケースがあります。

たとえば、Partial 型の結合結果が完全であることを実行時に検証するなど。

type Partial<T> = {

[P in keyof T]?: Partial<T[P]>
}

interface Person {
age: number,
name: string
}

const p1: Partial<Person> = { age: 29 }
const p2: Partial<Person> = { name: "na_o_ys" }

// Partial<Person> | Partial<Person> != Person なので, これはコンパイルエラー
const person: Person = { ...p1, ...p2 }

// 静的チェックを諦めて, 実行時に完全性チェックをしたいが,
// Person 型情報を実行時に取得できない
const person = { ...p1, ...p2 } as Person
for (const key of keyof Person) { // こんなシンタックスは無い
if (person[key] === undefined) throw "missing key"
}


解決法: 型を値として定義する

取得できないものは取得できないので、逆に型を値として定義することでこれを解決します。


準備

interface Value<T> {}

export function value<T>(): Value<T> {
return {}
}

type UnwrapValue<T> = T extends Value<infer U> ? U : never

export type Unwrap<T> = {
[P in keyof T]: UnwrapValue<T[P]>
}


型定義 & 実行時チェック

// 値として型を定義

const personStructure = {
age: value<number>(),
name: value<string>()
}

// 前書きの interface 定義と同値
type Person = Unwrap<typeof personStructure>

const p1: Partial<Person> = { age: 29 }
const p2: Partial<Person> = { name: "na_o_ys" }

// 実行時に完全性チェックができる!
const person = { ...p1, ...p2 } as Person
for (const key of Object.keys(personStructure)) {
if ((person as any)[key] === undefined) throw "missing key"
}


拡張: 再帰的にチェックする

本題です。

上記の定義だと、以下のようにネストした型を実行時に取得することができない。

// name のキー (first, last) を実行時に取得できない

const personStructure = {
age: value<number>(),
name: value<{
first: string,
last: string
}>()
}

おまじないを拡張してこれを可能にしよう。


準備

export function value<T>(): Value<T> {

return {}
}

export function values<T>(o: T): Values<T> {
return { _values: o }
}

interface Values<T> {
_values: T
}

interface Value<T> {}

type UnwrapValue<T> = T extends Value<infer U> ? U : never

type UnwrapValues<T, O = never> = T extends Values<infer U> ? Unwrap<U> : O

export type Unwrap<T> = {
[P in keyof T]: UnwrapValues<T[P], UnwrapValue<T[P]>>
}


型定義 & 実行時再帰的型チェック

const personStructure = {

age: value<number>(),
name: values({
first: value<string>(),
last: value<string>(),
})
}

// 以下の定義と同値
// interface Person {
// age: number,
// name: {
// first: string,
// last: string
// }
// }
type Person = Unwrap<typeof personStructure>

const p1: Partial<Person> = { age: 29 }
const p2: Partial<Person> = { name: { first: "na_" } }
const p3: Partial<Person> = { name: { last: "o_ys" } }

// deepMerge は別途定義
const person = deepMerge(p1, p2, p3) as Person
// 再帰的に完全性チェックができる!
function getMissingKeys(object: any, structure: any): string[] {
let missingKeys: string[] = []
for (const key in structure) {
if (structure.hasOwnProperty(key)) {
if (object[key] === undefined) {
missingKeys.push(key)
continue
}
if (object[key] !== undefined && structure[key]._values !== undefined) {
const childrenKeys = getMissingKeys(object[key], structure[key]._values).map(
childrenKey => `${key}.${childrenKey}`
)
missingKeys = missingKeys.concat(childrenKeys)
}
}
}
return missingKeys
}
getMissingKeys(person, personStructure) // 空配列が返る


まとめ

型情報はどうやっても実行時に取得できない。Conditional Types と型推論を利用することで、実行時に利用可能な構造と静的な型を同時に定義する方法を示した。

上記コードは TypeScript 3.0.1 で動作確認を行っているが、型推論器の気持ちになりきるのがまだ難しく、少し書き換えると動かなくなったりする。