TypeScript のいろんな型テクニック
型ガード
返り値の型を書く所に 値 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
を入れてるので、T2
はunion
になる。なぜこんなことをやっているかというと、T2
に分配前のunion
を保存しておくため。 -
T extends T2
で、T
のunion
の分配が起きる。 -
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 した値を返す例。
- A の配列の長さが T になるまで A の配列に 0 (値はなんでもいい)を足していく
- 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
の型を返す型
実装はここからのコピペ。
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
の名前をもとに、HTMLElementTagNameMap、SVGElementTagNameMap のマップ構造から該当の 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
以下は、渡された名前の配列に探したい名前があるかどうかを見つける型。
このような処理の流れになっている。
- ルックアップ型のキーで、0 か 1 かが選択されるようにする
- 項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でも紹介されてるイディオム的なやつ
never
never は値を持たない型。bottom型というやつ。
部分型付けシステムにおいて、ボトム型はすべての型の部分型である[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
が返ってきた際の挙動に気を付ける必要があるように感じた(感想)
this parameter、ThisType
下記の型は 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
内の this
は D
を参照できるようになる。
D
は data()
で返しているオブジェクトの型のことなので、具体的に言うと、 D
は以下の型
{
firstname: string;
lastname: string;
amount: number;
}
これを computed
内の this
から参照できるようになる。
なので、 ThisType
は返す型を定義しているわけではなく、その中で this
を参照した場合、何を返すかを定義しているイメージ。
そのため、この定義は
computed: C & ThisType<D>,
computed
は C
を返す。 computed
の中で this
を参照すると D
を返す。
というようなイメージ。
コード例では、もう一つ ThisType
を利用しているところがあった。
ここまでの内容がイメージできれば、この型も少し見やすくなる。
methods: M & ThisType<D & M & {[k in keyof C]: C[k] extends (...args: any[]) => infer R ? R : never }>,
methods
は M
を返す。 methods
の中で this
を参照すると D
と M
と C から生えてる関数
を返す。
この部分がキモく見えるかもしれないが、あくまで C
である computed
から生えてる関数の型を動的に定義しているだけ。
{[k in keyof C]: C[k] extends (...args: any[]) => infer R ? R : never }
Discussion