1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TypeScriptの型パズルを紹介する

Last updated at Posted at 2023-09-10

そもそも型パズルとは?

<「型」を受け取り「型」を出力する > 関数の型バージョンのようなものを作る問題。
今回それをやってみたのですが、TypeScriptに関する基礎的な知識があっても
最初独特の慣れが必要だなと感じたので書かせていただきます。

何が最初難しいのか

基本的に type YOUR_ANSWER = nantara
としてYOUR_ANSWERという型を用意することになります。
すると...のところでは例えば以下のことができません。

  • forループを回す
  • オブジェクトなどのインスタンスを直接参照する

型関数を作るときの入力は、所詮「型」でしかありません。
type YOUR_ANSWER<T> = nantara
として右辺でジェネリクスに入れたTをこねくり回すわけなのですが、
例えばTがオブジェクトの時、型情報に含まれる「オブジェクトのキー」を参照したいときはどうすればいいのでしょうか。

オブジェクトの型情報からキーを取り出すには

結論から書くと"in"と"keyof"を使います。

type Dog = {
    name: string;
    run: () => void;
};

type PartialDog = {
	[P in keyof Dog]?: Dog[P];
}

例えばこういったコードがあったとしたらP in keyof Dogとすることで
Dogのキーが順々に取り出されます。
PartialDog は { name?: string, run?: () => void }
というオブジェクトの型になります。

ではtupleのインデックスを取り出すには?

const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const

type result = TupleToObject<typeof tuple> 

このようにしたときにresultの持つ型情報が、
オブジェクト

{ tesla: 'tesla', 'model 3': 'model 3' //略 }

のようになるには、どうしたらいいのでしょうか。
いきなりだと難しいのでいくつかに分解します。
やることは、

  • tupleの各要素にアクセスする
  • それを順番にオブジェクトのキーにする
  • プロパティも同じ

ちなみにさっきの手は使えません。

type TupleToObject<T extends string[]> = {
  [P in keyof T] : T[P]
}

Tがさっきと違いtupleなので Pは順々にtupleのインデックスになります。

ちなみに自分はここで問題に触れました(製作者様とは何の関係もありません)

tupleが曖昧な人向け

概要

tuple型は[”型1”, “型2”, “型3”…]のように書きます。
特徴としては、一度定義されると

  • 要素の数が変わらない
  • 各要素の型が変わらない

ことです。

問題に戻る

tupleの型情報を順に取得するには?

流れとしてはこのようになります。

  • tupleの全ての要素をユニオンさせる
  • それを"in"で順に取り出す

コードとしては以下になります。

type TupleToObject<T extends string[]> = {
    [P in T[number]] : P
}

T[number]は 'tesla' | 'model 3' | 'model X' | 'model Y'
という型になります。「リテラル型」の「ユニオン型」です。

{
    [P in T[number]] : P
}

とすることで { tesla: 'tesla', 'model 3': 'model 3' // 略}
というオブジェクトが作られます。
(teslaだけクォートで囲まれない。model 3は間にスペースがあるので囲まれているだけ)
また、 P in T[number][]で囲まれているのは、この記法の約束だと思ってください。

またこちらの記事も参考になります。

答え

const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const

type TupleToObject<T extends readonly string[]> = {
  [K in T[number]] : K 
}
type result = TupleToObject<typeof tuple> // { tesla: 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}

tupleはreadonlyの配列か?

例えばさきほど、

type TupleToObject<T extends string[]> =

のように書きましたが

type TupleToObject<T extends readonly[]> =

もしtupleがreadonlyの配列ならこう書いてもいいのではないでしょうか。
ですがtupleは、
要素の合計数と 各「インデックス」に与えられた型
が変わらないだけです。インデックス1が、stringと指定されていたら、
それを新しい要素(同じく型はstring)に変更する事ができます。

ですが

type TupleToObject<T extends readonly string[]> = 

としたらどうでしょうか。これは "変更不可で全ての要素がstringの配列" を受け取ります。

(結局配列だが、例のタプルに最も近い定義だと考えられる)

実際先ほど貼りましたmosyaの回答では簡便のためかany []で制約されているのですが、
さらにその元ネタとなっているgithubの回答で最も評価を得ているのはextends readonly string[]でした。

補足 型定義にforループはない について

確かにforループは使えないのですが、Recursive Typesという概念があり、
型定義の中で「自分」を参照することで、リスト・ツリー・グラフなどのデータを表現できます。

ツリー構造のコードを紹介

type Tree<T> = {
  value: T;
  children: Tree<T>[];
};

const aTree: Tree<number> = {
  value: 1,
  children: [
    {
      value: 2,
      children: []
    },
    {
      value: 3,
      children: [
        {
          value: 4,
          children: []
        }
      ]
    }
  ]
};

具体的な例だとJSONに対しての定義に使われるみたいです。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?