LoginSignup
14
9

More than 3 years have passed since last update.

TypeScript: TupleToObject<T> ─ キーバリューのタプルからオブジェクトの型を作る方法

Last updated at Posted at 2019-12-04

やりたいこと

次のような[string, any]な型が複数あるとき、

type Foo = ['foo', string]
type Bar = ['bar', number]
type Buz = ['buz', boolean]

これらをもとにして、次のようなオブジェクト型を導き出したい。

type TheObject = {
    foo: string
    bar: number
    buz: boolean
}

やりかた

TupleToObjectというタプル型からオブジェクト型にマッピングする型を便宜上作ると良い。

type TupleToObject<T extends [string, any]> = {
    [K in T[0]]: Extract<T, [K, any]>[1]
}

この型の使い方は、TupleToObjectの型パラメータにタプル型をユニオン型で渡す。

type TheObject = TupleToObject<Foo | Bar | Buz>

動作確認

期待通りのオブジェクト型が導出されているか確認してみる。

// これがコンパイルに通り、
const obj1: TheObject = { foo: '', bar: 0, buz: false }

// これがコンパイルエラーになれば期待通り。
const obj2: TheObject = { foo: 0, bar: false, buz: '' }

TupleToObjectの仕組み

どのようにTupleToObjectが動くのかを見てみよう。

TupleToObjectの定義をもう一度見てみると、Extractを使っているのが分かる。

type TupleToObject<T extends [string, any]> = {
    [K in T[0]]: Extract<T, [K, any]>[1]
}

Extractがタプルをオブジェクトに変換する上で、重要な役割なのだが、それを説明する前に、TupleToObjectの失敗作を先に見せておきたい。

最初に作った失敗版TupleToObjectは次のような定義になっていた。

// 失敗作
type TupleToObject<T extends [string, any]> = {
    [K in T[0]]: T[1]
}

タプルの要素0をキーに、要素1を値にといった具合に率直な定義になっており、一見するとこれでも期待通りのオブジェクト型が導出できそうだ。しかし、実際使ってみると全然ダメだった。

この失敗作にTupleToObject<Foo | Bar | Buz>を当てはめてみると、値がすべてユニオン型になってしまったのだ。

type TheObject = {
    foo: string | number | boolean;
    bar: string | number | boolean;
    buz: string | number | boolean;
}

そのため、コンパイルエラーにしたい次のようなコードも、コンパイルが通ってしまった。

const obj2: TheObject = { foo: 0, bar: false, buz: '' }

この問題を解決してくれるのが、最初に重要だと述べたExtractだった。

type Extract<T, U>

Extractは2つの型パラメータを取り、T型がU型に代入可能であれば、その型を返し、そうでなければneverを返すConditional Typeだ。

例えば、次のようなコードではXの型はstringになる。というのも、string, number, booleanのうち、stringに代入できるのは、stringだけだからだ。

type X = Extract<string | number | boolean, string>

もちろん、タプル型にも応用できて、3つのタプルから特定のタプルを抽出することができる。

type X = Extract<['foo', string] | 
                 ['bar', number] | 
                 ['buz', boolean], ['foo', any]>

ここでのXの型は['foo', string]になる。

この抽出の仕掛けを失敗作に適用することで、キーに該当する値の型を抽出することができるようになる。

type TupleToObject<T extends [string, any]> = {
//  失敗作 
//  [K in T[0]]: T[1]
    [K in T[0]]: Extract<T, [K, any]>[1]
}

応用

キーバリューのタプルからオブジェクトの型を作る方法を応用すると、オブジェクトを合成したオブジェクトの型を作ることもできる。

const increment = {
    name: 'increment',
    calculate: (value: number): number => value + 1
} as const

const add = {
    name: 'add',
    calculate: (a: number, b: number): number => a + b
} as const

const sum = {
    name: 'sum',
    calculate: (...nums: number[]): number => nums.reduce((a, b) => a + b, 0)
} as const

interface Calculate<Name extends string = string> {
    name: Name
    calculate(...nums: number[]): number
}

type CalculatorOf<Method extends Calculate> = {
    [Name in Method['name']]: Extract<Method, Calculate<Name>>['calculate']
}

type TheCalculator = CalculatorOf<typeof increment | typeof add | typeof sum>

const calculator: TheCalculator = {
    [increment.name]: increment.calculate,
    [add.name]: add.calculate,
    [sum.name]: sum.calculate,
}

calculator.increment(1)
calculator.add(1, 2)
calculator.sum(1, 2, 3, 4, 5)

calculator.increment(1, 1) // コンパイルエラー: 引数が多い
calculator.add(1) // コンパイルエラー: 引数が足りない
calculator.sum(true, false, 'str') // コンパイルエラー: 引数の型が正しくない

参考資料

14
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
9