🐙

TypeScript のいろんな型テクニック

2022/02/22に公開約19,200字

型ガード

返り値の型を書く所に 値 is 期待する型 みたいな感じで書くやつ。
boolean を返す実装であればなんでもいいので、以下のような感じで、特定の型であることを確認するような実装を書く。

type Fruit = {
    name: string
}
type Apple = {
    name: "apple"
}
const isApple = (fruit: Fruit): fruit is Apple => {
  return fruit.name === "apple"
}
isApple({ name: "grape" }) // false
isApple({ name: "apple" }) // true

boolean を返す実装であればコンパイルは通るため、以下のような意図していない挙動になることもある。
その型ガードの実装が正しいかどうかは開発者自身で気をつける必要がある。

const isApple = (fruit: Fruit): fruit is Apple => {
  return fruit.name === "grape"
}
isApple({ name: "grape" }) // true

デフォルト型引数、可変長型引数

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]

配列の型からユニオンを作る

普通に配列を定義すると、型はプリミティブな型の配列になる

const arr = ["a","b","c"] // string[]

as const をつけると、値そのものの型になり、かつイミュータブル(readonly)な型になる。

const arr = ["a","b","c"] as const // readonly ["a", "b", "c"]

[number] のルックアップ型を利用すると、タプルを union に変換できる

const arr = ["a","b","c"] as const // readonly ["a", "b", "c"]
type union = (typeof arr)[number]  // "a" | "b" | "c"

[number] というのがちょっと驚きがあるが、頭の中のイメージとしては以下のような感じ。

  • (typeof arr)[0]"a"
  • (typeof arr)[1]"b"
  • (typeof arr)[0 | 1]"a" | "b"
  • (typeof arr)[number]"a" | "b" | "c"

配列に添字でアクセスしたら、そのインデックスにある値が取れる。添字の型は number みたいなイメージ。

&

& は 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> 
// {
//     gender: number;
// }

keyof O & keyof O1 で二つが共通して持っているキーの union が手に入る(上の例であれば、 "name" | "age"
Omit によって Foo & Bar から "name" | "age" のプロパティが消えて、差分である "gender" だけが残る

& は両方を満たす型を作ってくれるが、マージされた型ができるわけではない。

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

型のマージをしたいのであれば、こんな感じの型で包んであげればいい。

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

type hoge = Merge<{ a: 1 } & { b: 1 }>
// {
//   a: 1;
//   b: 1;
// }

|

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>
// {
//     a: number;
//     b: number;
//     c: boolean;
// }

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 を入れてるので、 T2union になる。なぜこんなことをやっているかというと、 T2  に分配前の union を保存しておくため。
  • T extends T2 で、Tunion の分配が起きる。
  • T<string | number> だった場合、 (string extends T2) | (number extends T2) という感じで T が分配される
  • その後の、 [T2] extends [T] はイメージとしては [分配前の型] extends [分配後の型] という比較をしている。
  • どういうことかというと、 union を条件分岐に利用すると分配されるが、 [] で包んであげれば union ではなくなるので分配は起きない。なので [T2] extends [T][T2] は元の union を維持するために [] で  T2 を包んでいる。
  • 対して、 [T] の方は分配後の型(union の個々の要素)が [] の中に入っている。
  • そのため、 [T2] extends [T] の判定は、 [string | number] extends [string] | [number] になる。
  • union でない場合はそもそも分配なんて挙動は起きてないので、 [T2] extends [T]true になる。
  • 逆に [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'
となる。

union distribution の使い所としては、union の型は保ったまま、条件に合致する型だけ何かしらの処理を行いたい、もしくは除外したい時に使うことができる。
以下は union の要素に "red" があれば、それを "wineRed" に更新する型と、除外する型

type Colors = "black" | "white" | "red"

type ModifyColorIfRed<T extends Colors> = T extends "red" ? "wineRed" : T
type OmitColorIfRed<T extends Colors> = T extends "red" ? never : T

type NewColors = ModifyColorIfRed<Colors> // "black" | "white" | "wineRed"
type NewColors2 = OmitColorIfRed<Colors> // "black" | "white"

分配が起こった結果、以下のような判定になる。

  • ModifyColorIfRed
    ("black" extends "red" ? "wineRed" : "black") | ("white" extends "red" ? "wineRed" : "white") | ("red" extends "red" ? "wineRed" : "red")

    "black" | "white" | "wineRed"

  • OmitColorIfRed
    ("black" extends "red" ? "wineRed" : "black") | ("white" extends "red" ? "wineRed" : "white") | ("red" extends "red" ? never : "red")

    "black" | "white" | never

    "black" | "white"

discriminated union

union のメンバーのどれに該当するかを判定するやつ

interface Square {
    kind: "square";
    size: number;
}

interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
type Shape = Square | Rectangle;

function area(s: Shape) {
    if (s.kind === "square") {
        // このブロック内では s は Square と推論される   
    }
    else {
        // このブロック内では s は Rectangle と推論される       
    }
}

参考:https://typescript-jp.gitbook.io/deep-dive/type-system/discriminated-unions

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>
// {
//   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";
// }

指定回数ループ処理をする型

型のロジックを考えるときに、「この回数分ループしたいなあ」みたいな発想になった際に使えるやつ。

以下は、与えられた型引数から -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

タプル

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

type huga = [1, 2, "ok", true]['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

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

以下の例は、引数や返り値に対して、infer を使っている例。

type Parameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never;
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;

以下の例は、型引数に対して、infer を使っている例。

type Person<T> = { name: T }
type PickName<V> = V extends Person<infer R> ? R : undefined
type bob = Person<'bob'>
type bobName = PickName<bob> // "bob"

ルックアップ型

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 型を求めることができる。

Template Literal Types

ただの文字列に何かしらの型推論を効かせることができる。
以下の例は、クエリのセレクタ文字列を受け取り、そのセレクタが指し示す HTMLElement の型を返す型
実装はここからのコピペ。

https://github.com/ghoullier/awesome-template-literal-types#documentqueryselector
type Split<S extends string, D extends string> = S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] : [S];
type TakeLast<V> = V extends [] ? never : V extends [string] ? V[0] : V extends [string, ...infer R] ? TakeLast<R> : never;
type TrimLeft<V extends string> = V extends ` ${infer R}` ? TrimLeft<R> : V;
type TrimRight<V extends string> = V extends `${infer R} ` ? TrimRight<R> : V;
type Trim<V extends string> = TrimLeft<TrimRight<V>>;
type StripModifier<V extends string, M extends string> = V extends `${infer L}${M}${infer A}` ? L : V;
type StripModifiers<V extends string> = StripModifier<StripModifier<StripModifier<StripModifier<V, '.'>, '#'>, '['>, ':'>;
type TakeLastAfterToken<V extends string, T extends string> = StripModifiers<TakeLast<Split<Trim<V>, T>>>;
type GetLastElementName<V extends string> = TakeLastAfterToken<TakeLastAfterToken<V, ' '>, '>'>;
type GetEachElementName<V, L extends string[] = []> = 
    V extends [] 
        ? L 
        : V extends [string] 
        ? [...L, GetLastElementName<V[0]>] 
        : V extends [string, ...infer R] 
        ? GetEachElementName<R, [...L, GetLastElementName<V[0]>]> 
        : [];
type GetElementNames<V extends string> = GetEachElementName<Split<V, ','>>;
type ElementByName<V extends string> = 
    V extends keyof HTMLElementTagNameMap 
        ? HTMLElementTagNameMap[V] 
        : V extends keyof SVGElementTagNameMap 
        ? SVGElementTagNameMap[V] 
        : Element;
type MatchEachElement<V, L extends Element | null = null> = 
    V extends [] 
        ? L 
        : V extends [string] 
        ? L | ElementByName<V[0]> 
        : V extends [string, ...infer R] 
        ? MatchEachElement<R, L | ElementByName<V[0]>> 
        : L;

type QueryResult<T extends string> = MatchEachElement<GetElementNames<T>>;

/**
 * Example
 */
declare function querySelector<T extends string>(query: T): QueryResult<T>;

const a = querySelector('div.banner > a.call-to-action') //-> HTMLAnchorElement
const b = querySelector('input, div') //-> HTMLInputElement | HTMLDivElement
const c = querySelector('circle[cx="150"]') //-> SVGCircleElement
const d = querySelector('button#buy-now') //-> HTMLButtonElement
const e = querySelector('section p:first-of-type'); //-> HTMLParagraphElement

ざっくりとした処理の流れのイメージは GetElementNames が渡されたクエリセレクタ文字列から、 HTMLElement の名前を抽出。
ElementByName はその HTMLElement の名前をもとに、HTMLElementTagNameMapSVGElementTagNameMap のマップ構造から該当の HTMLElement の型を探して返しているイメージ。

Template Literal Types を活用しているのはクエリセレクタ文字列をパースする処理の部分で、 GetLastElementName から始まる処理のあたり。

抜粋すると、以下の処理では、渡されたクエリセレクタ文字列に . があったら、 . の手前の文字列を返し、 # があったら # の手前の文字列を返し、、というのを ., #, [: の区切り文字に対しておこない、HTMLElement の名前を探そうとしている。

type StripModifier<V extends string, M extends string> = V extends `${infer L}${M}${infer A}` ? L : V;
type StripModifiers<V extends string> = StripModifier<StripModifier<StripModifier<StripModifier<V, '.'>, '#'>, '['>, ':'>;

awesome-template-literal-typesでは、このほかにも JSONのパーサーやSQL文のパーサーなどの実装例があり、 Template Literal Types で実現できるものがどういうものか、概観を知るのに良さげ。

再帰的な 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

never

never は値を持たない型。bottom型というやつ。

bottom 型の wiki には

部分型付けシステムにおいて、ボトム型はすべての型の部分型である[1] 。(ただしその逆は成り立たない。つまり、すべて型の部分型が必ずしもボトム型であるとはいえない。)

とあり、これらは全て true になる

type a = never extends any ? true : false
type b = never extends unknown ? true : false
type c = never extends string ? true : false
type d = never extends void ? true : false
type e = never extends null ? true : false
type f = never extends undefined ? true : false
type g = never extends never ? true : false

(ただしその逆は成り立たない。つまり、すべて型の部分型が必ずしもボトム型であるとはいえない。)

また、上記の引用の註釈にもあるように、先程の判定を逆にしてみると以下のような感じになって基本、false になった。

type a = any extends never ? true : false // boolean
type b = unknown extends never ? true : false // false
type c = string extends never ? true : false // false
type d = void extends never ? true : false // false
type e = null extends never ? true : false // false
type f = undefined extends never ? true : false // false
type g = never extends never ? true : false // true

この never を他の型と比較した際の挙動は頭の片隅に入れておいた方がいい気がしている。

例えば、上述の 「再帰的な Mapped Types」 でルックアップのキーを求めるところがこんな感じのコードになっており、neverとの比較を間に挟んでおり、これがどういう意味なのかを追っていく。
Last<T> extends never ? 1 : Last<T> extends U ? 0 : 1

ここの処理でやりたいことは T の末尾の要素が U と同じだったら 0、違ったら 1 を返したいだけなので、素直に書くと

Last<T> extends U ? 0 : 1

と書きそうになるが、 Last<T>T の配列が空だった場合は never を返す型。

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

そのため、T が空配列で Last<T> の結果が、 never となった場合、

never extends "太郎" ? 0 : 1

という比較になり、この比較は true になる( U に指定した名前がどんな名前であっても true になる)ので 0 が返ることになる。
これは望んでない挙動で、やりたいこととしては T の配列が空だったら、 1 を返すようにしたい。
そのため、 Last<T> の結果が、 never かどうかの確認を間に挟んでいる。

このような感じで、 never を返すこともある型を利用する型を作る場合は、never が返ってきた際の挙動に気を付ける必要があるように感じた(感想)

https://qiita.com/tkrkt/items/ecd7884ec1156017b00f
https://qiita.com/macololidoll/items/1c948c1f1acb4db6459e
https://typescriptbook.jp/reference/statements/never

this parameter、ThisType

https://github.com/type-challenges/type-challenges
下記の型は type-challenges の問題から抜粋
declare function SimpleVue<D, C, M>(options: { 
  data(this: {}): D, 
  computed: C & ThisType<D>, 
  methods: M & ThisType<D & M & {[k in keyof C]: C[k] extends (...args: any[]) => infer R ? R : never }>,
}): any

SimpleVue({
  data() {
    return {
      firstname: 'Type',
      lastname: 'Challenges',
      amount: 10,
    }
  },
  computed: {
    fullname() {
      return `${this.firstname} ${this.lastname}`
    },
  },
  methods: {
    getRandom() {
      return Math.random()
    },
    hi() {
      alert(this.fullname.toLowerCase())
      alert(this.getRandom())
    },
  },
})

this parameter

this parameter を使ってるのは上記のコードだと data(this: {}): D の部分。
これにより、 .data() が使えるのは SimpleVue だけで、他のコンテキストに持ち出しはできない、という制約を課すことができる。
ぱっと見、 .data() がなんらかの引数を取るように見えるがそうではなく、レシーバとなりえる this を指定しているだけ。そのため、 this parameter を利用したメソッドに何か引数を渡したい場合、第二引数以降で指定することになる。

ThisType

ThisType は ts の UtilityTypes の一つ。
上記のコードだとこのように利用している。

  computed: C & ThisType<D>, 

ThisType<D> これにより、 computed 内の thisD を参照できるようになる。
Ddata() で返しているオブジェクトの型のことなので、具体的に言うと、 D は以下の型

{
    firstname: string;
    lastname: string;
    amount: number;
}

これを computed 内の this から参照できるようになる。
なので、 ThisType は返す型を定義しているわけではなく、その中で this を参照した場合、何を返すかを定義しているイメージ。
そのため、この定義は

  computed: C & ThisType<D>, 

computedC を返す。 computed の中で this を参照すると D を返す。
というようなイメージ。

コード例では、もう一つ ThisType を利用しているところがあった。
ここまでの内容がイメージできれば、この型も少し見やすくなる。

  methods: M & ThisType<D & M & {[k in keyof C]: C[k] extends (...args: any[]) => infer R ? R : never }>,

methodsM を返す。 methods の中で this を参照すると DMC から生えてる関数 を返す。

この部分がキモく見えるかもしれないが、あくまで C である computed から生えてる関数の型を動的に定義しているだけ。

{[k in keyof C]: C[k] extends (...args: any[]) => infer R ? R : never }

https://typescriptbook.jp/reference/functions/this-parameters
https://www.typescriptlang.org/docs/handbook/utility-types.html#thistypetype

Discussion

ログインするとコメントできます