Open10

typescript の型メモ

poko tyanpoko tyan
  • デフォルト型引数、可変長型引数

T extends any[] = [] で T のデフォルトは [] になるので、再帰的なロジックを組む際の引数として利用できる

以下の例は、与えられた配列の型をフラットにした型を返す。
T は Flatten を利用する際は指定する必要がなく、Flatten の再帰処理において、結果を溜め込む場所として利用している

type Flatten<A extends any[], T extends any[] = []> = 
 A extends [infer Current, ...infer Rests] 
    ? Current extends Array<any> 
        ? [...T, ...Flatten<[...Current, ...Rests], T>] : [...T, Current, ...Flatten<[...Rests], T>] 
    : T;

Flatten<[1, 2, [3, 4], [[[5]]]]> // [1, 2, 3, 4, 5]
poko tyanpoko tyan
  • &

& は Intersection Types と呼ばれ、両方を満たす型を作ってくれる感じ。ベン図でいうところの、お互いが重なってる部分。

下記は Omit と組み合わせることで、差分部分の型を作るもの。

type Diff<O, O1> = Omit<O & O1, keyof O & keyof O1>

type Foo = {
  name: string
  age: string
}
type Bar = {
  name: string
  age: string
  gender: number
}

type hoge = Diff<Foo, Bar> 
// hoge の型はこんな感じ
// {
//     gender: number;
// }

& は両方を満たす型を作るので、そもそもが全く共通点がない型同士を & で繋いでも意味がない

type boo = { a: 1 } & { b: 1 } // 型は { a: 1 } & { b: 1 } のまま

なので、こんな感じの型で包んであげれば、オブジェクトのマージはできる。

type Copy<T> = {
  [K in keyof T]: T[K]
}

type hoge = Copy<{ a: 1 } & { b: 1 }>
// hoge の型
// {
//   a: 1;
//   b: 1;
// }
poko tyanpoko tyan
  • |

Union と呼ばれるやつ。

下記の例は index signature の部分にこの union を使って keyof F | keyof S したやつを in で回して、オブジェクトをマージした型を作る例

type Merge<F extends object, S extends object> = {
  [K in keyof F | keyof S]: K extends keyof S
    ? S[K]
    : K extends keyof F
    ? F[K]
    : never
}

type Foo = {
  a: number
  b: string
}
type Bar = {
  b: number
  c: boolean
}

type hoge = Merge<Foo, Bar>
// hoge はこんな型
// {
//     a: number;
//     b: number;
//     c: boolean;
// }
poko tyanpoko tyan
  • union distribution

型変数に union を渡して、かつその union が条件分岐に利用される場合、その条件が union の各要素に対して反映される。

これを利用して、渡された型が Union がどうかを確認する型の例が以下。

type IsUnion<T, T2 = T> = T extends T2
  ? [T2] extends [T]
    ? false
    : true
  : never

type hoge = IsUnion<string> // false
type huga = IsUnion<string|number> // true

T が <string | number> の union だった場合、

  • T2 にはデフォルトで T を入れてるので、T2 は union になる
  • T extends T2 で、分配が起きる。
  • T が <string | number> だった場合、 (string extends T2) | (number extends T2) という感じで T が分配される
  • そのため、 [T2] extends [T] の判定は、 [string | number] extends [string] | [number] になる
  • [T2] extends [T] の判定が false になるものは union と判断できる

以下の例は、型引数 T に渡した union を型引数 U に合致するもののみ残した union 型を作るもの。

type Extract<T, U> = T extends U ? T : never;
type T1 = 'foo' | 'bar' | 0 | false;
type T2 = Extract<T1, string>; // T2は 'foo' | 'bar' 型になる

T1 に union である 'foo' | 'bar' | 0 | false を渡しており、かつ条件判定に利用しているので、分配が起こる。

分配が起こった結果、以下のような判定になり、
('foo' extends U ? 'foo' : never) | ('bar' extends U ? 'bar' : never) | (0 extends U ? 0 : never) | (false extends U ? false : never)

さらに上記の例では U には string を渡しているので、以下のようになる。
('foo' extends string ? 'foo' : never) | ('bar' extends string ? 'bar' : never) | (0 extends string ? 0 : never) | (false extends string ? false : never)

この判定を結果を求めると、
'foo' | 'bar' | never | never => 'foo' | 'bar'
となる。

https://numb86-tech.hatenablog.com/entry/2020/07/02/103544

poko tyanpoko tyan
  • index signature

ゆるく、オブジェクトの型を指定するやつ。

let obj: {
  [K: string]: number;
};

こうすれば、obj は string のフィールド、バリューは number ならなんでもOK、みたいな型になる

以下の例は、このインデックスシグネチャがあった場合、それを除外した型を作る例

type RemoveIndexSignature<T> = {
  [P in keyof T as P extends `${infer A}` ? A : never]: T[P]
}
    
type Bar = {
  [key: number]: any;
  bar(): void;
}
  
type hoge = RemoveIndexSignature<Bar>
// hoge はこんな型
// {
//   bar: () => void;
// }

P in keyof T as P extends こんな感じで、そのオブジェクトのキー一つ一つに対して、条件判定させるのは、イディオム的に覚えておくと便利そう。
例えば、以下は引数で渡されたオブジェクトの型に指定された型を値として持ってれば、そのフィールドを omit する。

type OmitByType<T extends {}, U> = {
  [k in keyof T as T[k] extends U ? never : k]: T[k]
}

interface Model {
  name: string
  count: number
  isReadonly: boolean
  isEnable: boolean
}

type hoge = OmitByType<Model, boolean>
// {
//    name: string;
//    count: number;
// }

このindex signature で as を使った例をもう一つ。
キーとバリューを入れ替えた型を作る例。

type Flip<T extends Record<any, any>> = {
  [key in keyof T as T[key] | `${T[key]}`]: key;
};

type hoge = Flip<{a: 'b'}>
// {
//   b: "a";
// }
poko tyanpoko tyan
  • 指定回数ループ処理をする型

与えられた型引数から -1 した値を返す例。

  1. A の配列の長さが T になるまで A の配列に 0 (値はなんでもいい)を足していく
  2. 1が真になったらAの配列から値を一つ削除(Pop)し、Aの配列の長さを結果として返す
type Pop<T extends any[]> = T extends [...infer head, any] ? head : never; 

type MinusOne<T extends number, A extends any[] = []> = A['length'] extends T
  ? Pop<A>['length']
  : MinusOne<T, [...A, 0]>

MinusOne<1> // 0
MinusOne<55> // 54

上の MinusOne と同じような発想(ループで配列に値を詰めて、その配列の長さを判定に使う)でできてる例をもう一つ。
与えられた文字列の長さを返す例。

type Shift<T extends string> = T extends `${infer _}${infer Rest}`
  ? Rest
  : T

type Length<T extends string, A extends any[] = []> = 
  T extends ''
    ? A['length']
    : Length<Shift<T>, [...A, 0]>

Length<'aaaaaa'> // 6
poko tyanpoko tyan
  • タプル

固定長の配列の型。
固定長なので、['length'] の結果がその配列の要素の数そのものになる。

type huga = [1, 2, 3, 4]['length']
// huga の型は 4。number ではい。

これを利用して、渡された型がタプルかどうかを判定するのが以下の例。

type IsTuple<T> = T extends readonly any[]
  ? number extends T['length']
    ? false
    : true
  : false

type hoge = IsTuple<[1]> // true
type huga = IsTuple<{ length: 1}> // false
poko tyanpoko tyan
  • infer

型の値をキャプチャして再利用するイメージ。

以下の例は、渡された配列の末尾の要素を返す型。

type Last<T extends any[]> = T extends [...any, infer Rest]
  ? Rest
  : never

type Last2<T extends any[]> = T extends [any, ...infer Rest]
  ? T[Rest['length']]
  : never

type hoge = Last<[3, 2, 1]> // 1
type huga = Last2<[3, 2, 1]> // 1

Last と Last2 はロジックは違うがやりたいことは同じ。

Last は素直に最後の要素を Infre Rest でキャプチャし、それを結果として返している
Last2 は最初の要素以外を Rest でキャプチャしているので、Rest にキャプチャされているのは配列。その配列の length を T の index に使うことで、最後の要素を結果として返している。

以下の例は string の型に対して、infer を使っている例。
与えられたstringに ' ' | '\n' | '\t' が存在すればトリムして返す型。

type TrimPattern = ' ' | '\n' | '\t'

type ExistsTrimPattern<S extends string> = S extends `${string}${TrimPattern}`
 ? true
 : S extends `${TrimPattern}${string}`
   ? true
   : false

type Trim<S extends string> = ExistsTrimPattern<S> extends true
  ? TrimRight<TrimLeft<S>>
  : S

type TrimRight<S extends string> = S extends `${infer T}${TrimPattern}`
  ? TrimRight<T>
  : S

type TrimLeft<S extends string> = S extends `${TrimPattern}${infer T}`
  ? TrimLeft<T>
  : S

Trim<'str'> // str
Trim<' str'> // str
Trim<'     str'> // str
Trim<'str   '> //str
poko tyanpoko tyan
  • ルックアップ型
interface Person {
  name: string;
}

Person["name"] // string;

オブジェクトにプロパティアクセスして、要素の型を求める感じのやつ。
上記の例は、静的にプロパティ名 name を指定しているが、動的にもできる

interface Person {
  name: string;
  age: number;
}

type hoge = Person[keyof Person] // string | number

keyof Person で Person のキーの union 型を用いてオブジェクトをルックアップすることで結果、Person のオブジェクトの値の union 型を求めることができる。

poko tyanpoko tyan
  • 再帰的な Mapped Types
  1. ルックアップ型のキーで、0 か 1 かが選択されるようにする
  2. 項1 の結果、0 と 1 のどちらが選ばれたかで、Mapped Types の処理を分岐させる(再帰処理を続けるか、終了するか)

以下は、渡された名前の配列に探したい名前があるかどうかを見つける型。

type Last<T extends any[]> = T extends [...any, infer Rest] ? Rest : never
type Pop<T extends any[]> = T extends [...infer Head, any] ? Head : never
type Exsits<T extends any[]> = T['length'] extends 0 ? false : true

type Find<T extends any[], U extends string> = {
  0: 'いる'
  1: Exsits<T> extends true ? Find<Pop<T>, U> : 'いない'
}[Last<T> extends never ? 1 : Last<T> extends U ? 0 : 1]

type hoge = Find<['太郎', '二郎', '三郎'], '五郎'> // いない
type huga = Find<['太郎', '二郎', '三郎'], '三郎'> // いる

この例では 0 や 1 を使ったが、ルックアップ型で判定した結果返した値を、Mapped Types のキーで即利用すればいいだけなので、こんな感じでもいい。

type Find<T extends any[], U extends string> = {
  ['探索終了']: 'いる'
  ['探索続行']: Exsits<T> extends true ? Find<Pop<T>, U> : 'いない'
}[Last<T> extends never ? '探索続行' : Last<T> extends U ? '探索終了' : '探索続行']

issueでも紹介されてるイディオム的なやつ
https://github.com/Microsoft/TypeScript/issues/14833
https://qiita.com/hrsh7th@github/items/84e8968c3601009cdcf2