型制約 extends#
構文:T extends K
、ここでの extends
はクラスやインターフェースの継承ではなく、型の判断と制約を意味し、T
が K
に代入できるかどうかを判断します。
T
が U
に代入できるかを判断し、可能であれば T
を返し、そうでなければ never
を返します:
type Exclude<T, U> = T extends U ? T : never
型マッピング in#
指定されたインターフェースのキーを反復処理するか、またはユニオン型を反復処理します:
interface Person {
name: string
age: number
gender: number
}
// T のすべてのプロパティを読み取り専用型に変換
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
// type ReadonlyPerson = {
// readonly name: string
// readonly age: number
// readonly gender: number
// }
type ReadonlyPerson = Readonly<Person>
型述語 is#
TypeScript には型保護メカニズムがあります。型保護を定義するには、単純に関数を定義し、その戻り値を型述語にします:
function isString(test: any): test is string {
return typeof test === 'string'
}
上記の書き方は、戻り値が boolean 値の関数を書くことと何が違うのでしょうか?
function isString(test: any): boolean {
return typeof test === 'string'
}
is 型保護を使用する場合:
function isString(test: any): test is string {
return typeof test === 'string'
}
function example(foo: any) {
if (isString(foo)) {
console.log('it is a string' + foo)
console.log(foo.length) // string function
// 以下のコードはコンパイル時にエラーが発生し、実行時にもエラーが発生します。なぜなら foo は string で toExponential メソッドが存在しないからです
console.log(foo.toExponential(2))
}
// コンパイル時にはエラーが発生しませんが、実行時にエラーが発生します
console.log(foo.toExponential(2))
}
example('hello world')
戻り値が boolean の場合:
function isString(test: any): boolean {
return typeof test === 'string'
}
function example(foo: any) {
if (isString(foo)) {
console.log('it is a string' + foo)
console.log(foo.length) // string function
// foo は any なので、コンパイルは正常ですが、実行時にエラーが発生します。なぜなら foo は string で toExponential メソッドが存在しないからです
console.log(foo.toExponential(2))
}
}
example('hello world')
まとめ:
- 型保護を使用すると、TS は変数の型をさらに絞り込みます。例では、型が any から string に絞り込まれました;
- 型保護の作用域は、if の後のブロックレベルの作用域内でのみ有効です。
実践:
function isAxiosError(error: any): error is AxiosError {
return error.isAxiosError
}
if (isAxiosError(err)) {
code = `Axios-${err.code}`
}
待推論型 infer#
infer P
を使用して、推論待ちの型としてマークされたジェネリックを示し、直接使用できます。
関数の引数の型を取得:
type ParamType<T> = T extends (param: infer P) => any ? P : T
type FunctionType = (value: number) => boolean
type Param = ParamType<FunctionType> // type Param = number
type OtherParam = ParamType<symbol> // type Param = symbol
T
が (param: infer P) => any
に代入できるかを判断し、引数をジェネリック P
として推論します。代入可能であれば引数の型 P
を返し、そうでなければ渡された型を返します。
関数の戻り値の型を取得:
type ReturnValueType<T> = T extends (param: any) => infer U ? U : T
type FunctionType = (value: number) => boolean
type Return = ReturnValueType<FunctionType> // type Return = boolean
type OtherReturn = ReturnValueType<number> // type OtherReturn = number
T
が (param: any) => infer U
に代入できるかを判断し、戻り値の型をジェネリック U
として推論します。代入可能であれば戻り値の型 P
を返し、そうでなければ渡された型を返します。
原始型保護 typeof#
構文:typeof v === 'typename'
または typeof v !== 'typename'
、データの型が特定の原始型(number
、string
、boolean
、symbol
)であるかどうかを判断し、型保護を行います。
typename は number
、string
、boolean
、symbol
でなければなりません。しかし、TypeScript は他の文字列との比較を妨げることはありません。言語はそれらの式を型保護として認識しません。
例:print 関数は引数の型に応じて異なる結果を印刷しますが、引数が string
か number
かをどうやって判断するのでしょうか?
function print(value: number | string) {
// もし string 型なら
// console.log(value.split('').join(', '))
// もし number 型なら
// console.log(value.toFixed(2))
}
一般的な判断方法は二つあります:
split
プロパティがあるかどうかでstring
型を判断し、toFixed
メソッドがあるかどうかでnumber
型を判断します。欠点:判断や呼び出しの際に型変換を行う必要があります。- 型述語
is
を使用します。欠点:毎回ツール関数を書く必要があり、面倒です。
したがって、ここでは typeof
を使用できます:
function print(value: number | string) {
if (typeof value === 'string') {
console.log(value.split('').join(', '))
} else {
console.log(value.toFixed(2))
}
}
typeof
を使用して型を判断した後、TypeScript は変数をその具体的な型に絞り込みます。この型が変数の元の型と互換性がある限り。
型保護 instanceof#
typeof
と似ていますが、作用の仕方が異なります。instanceof
型保護は、コンストラクタを通じて型を絞り込む方法です。
instanceof
の右側にはコンストラクタが必要で、TypeScript は以下のように絞り込みます:
- このコンストラクタの
prototype
プロパティの型、もしその型がany
でない場合 - コンストラクタのシグネチャが返す型のユニオン
class Bird {
fly() {
console.log('Bird flying')
}
layEggs() {
console.log('Bird layEggs')
}
}
class Fish {
swim() {
console.log('Fish swimming')
}
layEggs() {
console.log('Fish layEggs')
}
}
const bird = new Bird()
const fish = new Fish()
function start(pet: Bird | Fish) {
// layEggs を呼び出すのに問題はありません。なぜなら Bird または Fish のどちらも layEggs メソッドを持っているからです
pet.layEggs()
if (pet instanceof Bird) {
pet.fly()
} else {
pet.swim()
}
// 以下と同じです
// if ((pet as Bird).fly) {
// (pet as Bird).fly();
// } else if ((pet as Fish).swim) {
// (pet as Fish).swim();
// }
}
インデックス型クエリ演算子 keyof#
構文:keyof T
、任意の型 T
に対して、keyof T
の結果は T
上の既知の公共プロパティ名のユニオンです。
keyof
は Object.keys にやや似ていますが、keyof はインターフェースのキーを取得します。
interface Point {
x: number
y: number
}
// type keys = "x" | "y"
type keys = keyof Point
以下のようなオブジェクトがあると仮定し、その属性値を取得するために TypeScript で get 関数を実装する必要があります:
function get(o: object, name: string) {
return o[name]
}
最初はこのように書くかもしれませんが、多くの欠点があります:
- 戻り値の型を確認できません:これにより、ts の最大の型検証機能が失われます;
- key に制約をかけることができません:スペルミスの問題が発生する可能性があります。
この時、get 関数の型機能を強化するために keyof を使用できます:
function get<T extends object, K extends keyof T>(o: T, name: K): T[K] {
return o[name]
}
注意が必要なのは、keyof
は型上で既知の公共プロパティ名のみを返すことができるということです:
class Animal {
type: string
weight: number
private speed: number
}
type AnimalProps = keyof Animal // 'type' | 'weight'
オブジェクトの特定の属性値を取得する必要があるが、どの属性か不明な場合、この時は extends
と typeof
を組み合わせて属性名を制限し、渡される引数がオブジェクトの属性名のみであることを制限できます:
const person = {
name: 'Jack',
age: 20,
}
function getPersonValue<T extends keyof typeof person>(fieldName: keyof typeof person) {
return person[fieldName]
}
const nameValue = getPersonValue('name')
const ageValue = getPersonValue('age')