Advent Calendar of TypeScript
Day | Title |
---|---|
12/1 | オブジェクトの分割代入 |
12/2 | 配列の分割代入 |
12/3 | 文字列はプリミティブ値なのにlengthプロパティをもつ? |
12/4 | 関数型の表現方法をまとめる |
12/5 | インデックスシグネチャと型安全性 |
12/6 | Mapの型 |
12/7 | WeakMapとガベージコレクション |
12/8 | readonly でプロパティを読み取り専用に |
12/9 | readonlyなプロパティは関数を通して変更されうる |
12/10 | 型における空集合、never型とは |
12/11 | 部分型って"拡張型"じゃない?集合論で捉えてみる |
12/12 | ユーティリティ型(組み込み型)、PickとOmit |
12/13 | 組み込み型(Extract、Exclude、Partial、Required、NonNullable) |
12/14 | オプショナルプロパティ"?:"と"undefinedとのユニオン型"は同じもの? |
12/15 | リテラル型と型推論のwidening |
12/16 | どっちのtypeof? |
12/17 | オブジェクト型を支えるlookup型とkeyof型 |
12/18 | ユーザー定義型ガード |
12/19 | Mapped Typesの基礎 |
12/20 | Conditional Typesの基礎 |
12/21 | PickやPartialなどのユーティリティ型はどのように作られているか? |
12/22 | Mapped Typesを集合と写像の観点から捉える |
12/23 | Homomorphic Mapped Types と準同型写像 |
12/24 | Record型やReturnType型などのユーティリティ型をみてみる |
12/25 | inferとはなにものか?Zodを添えて |
12/1 オブジェクトの分割代入
オブジェクトの分割代入構文を用いると、オブジェクトの値を取り出す処理を比較的簡潔に書けます。
const object = {
foo: 271,
bar: "str",
"1234": 314,
}
const { foo, bar } = object
console.log(foo) //271
console.log(bar) //str
変数名と同じプロパティ名に対応する値が変数に代入されます。
ただし、型注釈をつけることはできず、変数の型は型推論によって決定されます。
識別子でないプロパティ名
プロパティ名が識別子ではない場合(数字で始まっていたり、特定の記号を含んでいる場合)には、変数名を変更することで分割代入可能となります。
const object = {
foo: 271,
bar: "str",
"1234": 314,
}
const { foo, "1234": var1234 } = object
console.log( foo ) // 123
console.log( var1234 ) // 314
ネストされているオブジェクト
ネストされているオブジェクトから値を取り出したいときにも活用できます。
const nestedObj = {
foo: 123,
obj: {
bar: 1,
obj2: {
baz: 2,
},
},
}
const { obj: {bar, obj2: { baz }}} = nestedObj
console.log(bar)//1
console.log(baz)//2
12/2 配列の分割代入
配列についても分割代入が使用可能です。
オブジェクトの場合と同じで型注釈をつけることはできず、型推論のみによって型が決定されます。
const arr = ["hello", 123, true]
const [foo, bar] = arr
console.log(bar) //123
オブジェクトの分割代入と組み合わせることで、簡潔に値を取り出せます。
const arr = ["hello", 123, true]
const [foo, bar] = arr
const obj = { foo, arr }
const {
arr: [x, y],
} = obj
console.log(x) //hello
console.log(y) //123
配列の中にオブジェクトが含まれている場合に、そのオブジェクトの一部だけを取り出すのに使うこともできます。
const arrayObj = [{ a: 1, b: 2 }, { c: 3 }]
const [{b},{c}] = arrayObj
console.log(b) //2
console.log(c) //3
12/5 インデックスシグネチャと型安全性
オブジェクトのプロパティ型の記法の一つにインデックスシグネチャというものがあります。
{[key名: string]: プロパティの型}
のように記述することで、オブジェクトに含まれるプロパティの名前や個数が決まっていない場合に対応することができます。
type Obj = {
[k: string]: number | string
}
const obj : Obj = {
foo: 1,
bar: 2,
baz:'3'
}
型安全性
オブジェクトの型を動的に設定するのに便利な機能ですが、型安全性を損なう使い方ができてしまう点には注意する必要があります。具体的には、プロパティ名を動的に決めた場合、存在しないプロパティへのアクセス時にコンパイルエラーが発生しなくなってしまうというものです。
const str : string = 'foo'
const obj = {
one: 1,
two: 2,
[str]: 'test'
}
//型は以下のように推論される
//const obj: {
// [x: string]: string | number;
// one: number;
// two: number;
// }
console.log(obj.foo) // "test"
console.log(obj) //{"one": 1, "two": 2, "foo": "test"}
//存在しないプロパティにアクセスしても型エラーが出ない。
console.log(obj.notExistProp) //undefined
この例では、foo: string
という型のプロパティを設定しているはずなのに、任意のプロパティ名の存在を許してしまっています。実行時エラーにつながってしまう書き方もできるため、Map
を使用することが推奨されています。
Mapの型
Map
Map
はキーとそれに対応する値を保持する機能を持つオブジェクトです。
キーとして任意の値を用いることができ、オブジェクトをキーとすることも可能です。
また、Map
の型はMap<K, V>
のように表されます(Kはキーの型、Vは値の型です)。
Mapの操作
Map
にキーと値を追加するにはset
メソッドを利用します。
また、Map
に格納された値を取得するget
メソッドや、引数として渡したキーがMap
に保持されているかを判定できるhas
メソッドなどもあります。
const map: Map<string|KeyObj,number> = new Map()
type KeyObj = {key: string}
const keyObj: KeyObj = {key: 'value'}
map.set('key1',1234)
map.set(keyObj,210)
console.log(map) //Map (2) {"key1" => 1234, {"key": "value"} => 210}
console.log(map.has('key1')) //true
console.log(map.get('key1')) //1234
Mapの値を列挙する
Map
にはkeys
、values
、entries
といった、保持している値を列挙するためのメソッドも備わっています。これらのメソッドの返り値はイテレータと呼ばれるもので、ループ処理での使用頻度が高いです。
const map: Map<string|KeyObj,number> = new Map()
type KeyObj = {key: string}
const keyObj: KeyObj = {key: 'value'}
map.set('key1',1234)
map.set(keyObj,210)
for(const key of map.keys()){
console.log(key)
}
//"key1"
//{"key": "value"}
for(const value of map.values()){
console.log(value)
}
//1234
//210
for(const[key, value] of map.entries()){
console.log(key)
console.log(value)
}
//"key1"
//1234
//{"key": "value"}
//210
12/7 WeakMapとガベージコレクション
WeakMap
Map
に似たオブジェクトにWeakMap
というものが存在します。
Map
と違って列挙を行うためのメソッド(keys
、values
、entries
)を持たず、キーとしてはオブジェクト(もしくはnon-registered なシンボル)のみを設定することができ、数値や文字列を設定することはできません。
const weakMap = new WeakMap()
//Argument of type 'string' is not assignable to parameter of type 'object'.ts(2345)
weakMap.set('str','test')
//実行時エラーが発生(Invalid value used as weak map key)
ガベージコレクション
WeakMap
がMap
と異なる点として、キーへの参照が弱参照である点があります。
このことは、キーとして設定できる値がオブジェクトかnon-registered なシンボルしか許されていない理由の一端となっています。
registered なシンボルはガベージコレクションの対象とならないため、WeakMap
のキーとしての使用が許可されていないようです。
Because registered symbols can be arbitrarily created anywhere, they behave almost exactly like the strings they wrap. Therefore, they are not guaranteed to be unique and are not garbage collectable. Therefore, registered symbols are disallowed in WeakMap, WeakSet, WeakRef, and FinalizationRegistry objects.
Map
でキーとして使用されているオブジェクトは、Map
自体がガベージコレクトされない限りはガベージコレクションの対象とならず、メモリ上に保持され続けます。これは、Map
がキーを列挙するメソッドを持っているため、キーとして登録されたオブジェクトが不要になったと判断されないためです。
WeakMapまとめ
キーを列挙するメソッドを持たないようにしたり、弱参照とならないキーを設定できないようにするなど、Map
の機能にいくつかの制約をつけることでメモリの節約ができるようにしたオブジェクトがWeakMap
であると言えます。
12/8 readonly でプロパティを読み取り専用に
readonly
オブジェクトのプロパティを読み取り専用にする機能として、readonly
というものがあります。プロパティ名の前にreadonly
をつけることで、そのプロパティの値に代入しようとするとコンパイルエラーが発生するようになります。
const obj: {
num: number
readonly readonlyNum: number
} = {
num:314,
readonlyNum: 271
}
obj.num = 3141
obj.readonlyNum = 2718 // Cannot assign to 'readonlyNum' because it is a read-only property.(2540)
Readonly
型
オブジェクトのプロパティ全体を読み取り専用にするReadonly<T>
という組み込みの型を使用すれば、オブジェクトのプロパティすべてを読み取り専用にできます。
type readonlyObj = Readonly<{
num: number
name: string
}>
const obj: readonlyObj = {
num: 314,
name: 'pi'
}
obj.num = 1 // Cannot assign to 'num' because it is a read-only property.(2540)
obj.name = 'one'// Cannot assign to 'name' because it is a read-only property.(2540)
Readonly<T>
はT
のプロパティをすべてreadonly
にしますが、プロパティがオブジェクトだった場合、そのオブジェクトに含まれるプロパティまではreadonly とならないので注意する必要があります(オブジェクト自体がreadonly となります)。
type readonlyObj = Readonly<{
num: number
numObj: {
name: string
num: number
}
}>
const obj: readonlyObj = {
num: 314,
numObj: {
name: "two",
num: 2,
},
}
obj.numObj.num = 3 //コンパイルエラーが発生しない
obj.numObj = {name: 'three', num: 3} //Cannot assign to 'numObj' because it is a read-only property.
as const
ネストの深い階層のプロパティも含めてreadonlyにしたい場合には、as const
を使用する手もあります。
as const
は、オブジェクトリテラルのプロパティを再帰的にreadonlyとします(型にas const
をつけられるわけではない点に注意)。型アサーションとは異なり、型安全性を損なう機能ではないため、積極的に使用していけます。
const obj = {
obj1: { obj2: { obj3: { num: 3, name: 'three' }}}
} as const
obj.obj1.obj2.obj3.name = 'four' // Cannot assign to 'name' because it is a read-only property.(2540)
型として使用したい場合には、as const
で読み取り専用とした値からtypeof
によって型を作成することができます。
const obj = {
obj1: { obj2: { obj3: { num: 3, name: 'three' }}}
} as const
type NestedObj = typeof obj
NestedObj
型は以下のようになります。
type NestedObj = {
readonly obj1: {
readonly obj2: {
readonly obj3: {
readonly num: 3;
readonly name: "three";
};
};
};
}
12/9 readonlyなプロパティは関数を通して変更されうる
オブジェクトをreadonly
なプロパティとしたときには、そのプロパティに値を代入しようとするとコンパイルエラーが発生するようになります。
type ReadonlyObj = {
readonly num: number
}
const obj: ReadonlyObj = {
num: 314
}
obj.num=271 //Cannot assign to 'num' because it is a read-only property.(2540)
しかし、以下のようにreadonly
ではないプロパティを持つオブジェクト型を引数の型とする関数を経由して同様の処理を行うと、コンパイルエラーが起こることなくプロパティの値を変更できてしまうので注意が必要です。
type Obj = {
num: number
}
type ReadonlyObj = {
readonly num: number
}
const obj: ReadonlyObj = {
num: 314
}
const changeNum = (obj: Obj)=>{
return obj.num = 271
}
console.log(obj) //{ "num": 314 }
changeNum(obj) // readonlyなプロパティを(コンパイルエラーなしで)書き換えられてしまう。
console.log(obj) //{ "num": 271 }
12/12 ユーティリティ型(組み込み型)、PickとOmit
TypeScriptの標準ライブラリには、組み込み型、ユーティリティ型と呼ばれる汎用性の高い型が用意されています。以前に紹介したReadonly<T>
型も組み込み型の一つです。
Pick<T, K>
型とOmit<T, K>
型
Pick型は、オブジェクト型T
からK
で指定したキーをもつプロパティを取り出して新しいオブジェクト型を作り出す組み込み型です。K
にはT
型のキーをユニオン型で指定できます。
type T = { str: string, num: number, obj: { foo: string, bar: boolean } }
//type P = { num: number; str: string;}
type P = Pick<T,"num"|"str">
//type Q = { obj: { foo: string; bar: boolean;};}
type Q = Pick<T,"obj">
//type R = { foo: string; }
type R = Pick<T["obj"], "foo">
作成される型はオブジェクト型であることに注意が必要ですね。
Omit型はPick型とは反対に、K
で指定したキーをもつプロパティを除いたオブジェクト型を返します。
type T = { str: string, num: number, obj: { foo: string, bar: boolean } }
//type P = { obj: { foo: string; bar: boolean;};}
type P = Omit<T, "num" | "str">
//type Q = { num: number; str: string;}
type Q = Omit<T, "obj">
//type R = { bar: boolean;}
type R = Omit<T["obj"], "foo">
Pick<T, K>
のK
にはK extends keyof T
という制約が課されており、T
のキーの部分型を指定できます。一方でOmit<T, K>
のK
に課されている制約はK extends keyof any
となっており、PickのK
よりも条件の緩い指定ができるようになっています。
12/13 組み込み型(Extract、Exclude、Partial、Required、NonNullable)
Extract<T, U>
型、Exclude<T, U>
型
Extract<T, U>
は型T
から型U
の部分型となる型を抽出する組み込み型です。抽出できないような型の指定がされた場合にはnever
型が返されます。
T
としてはユニオン型が指定されることが多いですが、必ずしもユニオン型である必要はありません。
type T = { str: string, num: number }
type U = T | number | boolean | "314"
//type P = "314"
type P = Extract<U, string>
//type Q = { str: string; num: number;}
type Q = Extract<T, {str: string}>
//type R = never
type R = Extract<U, {foo: boolean}>
Exclude<T, U>
型は、Extract<T, U>
とは反対に、型T
から型U
の部分型となる型を取り除く組み込み型です。すべての型情報が取り除かれるような型U
が指定された場合には、never
型が返されます。
type T = { str: string, num: number }
type U = T | number | boolean | "314"
//type P = number | boolean | T
type P = Exclude<U, string>
//type Q = never
type Q = Exclude<T, {str: string}>
//type R = number | boolean | T | "314"
type R = Exclude<U, {foo: boolean}>
Partial<T>
型とRequired<T>
型
Partial<T>
型は、型T
のプロパティをオプショナルにする型です。
type T = { str: string, num: number }
// type P = {
// str?: string | undefined
// num?: number | undefined
// }
type P = Partial<T>
//type Q = number プリミティブな型はそのまま返ってきている。
type Q = Partial<number>
Readonly
型と同様に、オブジェクトのプロパティを再帰的にオプショナルにするわけではない点に注意が必要です。
type NestedObj = { str: string, obj:{ str: string, num: number } }
//type R = {
// str?: string | undefined;
// obj?: {
// str: string;
// num: number;
// } | undefined;
// }
type R = Partial<NestedObj>
Required
型は、Partial
型とは逆で、オプショナルな型をオプショナルではなくします。こちらも再帰的にオプショナル型ではなくすわけではないことに注意が必要です。
type T = { str?: string, num: number }
//type P = { str: string; num: number; }
type P = Required<T>
//type Q = number
type Q = Required<number>
type NestedObj = { str?: string, obj?:{ str?: string, num: number } }
// type R = {
// str: string;
// obj: {
// str?: string;
// num: number;
// };
// }
type R = Required<NestedObj>
NonNullable<T>
型
NonNullable<T>
型は型T
からnull
とundefined
を取り除いた型となります。オブジェクトのプロパティがnull
の場合に取り除いたりはしません。
//type P = string
type P = NonNullable<string | null | undefined>
type T = null | {str: string, n: null}
//type Q = { str: string; n: null; }
type Q = NonNullable<T>
NonNullable
の元の実装を見てみると、type NonNullable<T> = T & {};
というようになっています。{}
という型がnull
もしくはundefined
以外の部分型であることを利用して、null
とundefined
を取り除いているわけですね。
12/14 オプショナルプロパティ"?:"と"undefinedとのユニオン型"は同じもの?
オプショナルプロパティとは、オブジェクト型の機能の一つで、プロパティが存在しない可能性があることを型情報として明示できます。
存在しないプロパティにアクセスした場合には通常はコンパイルエラーが出ますが、オプショナルプロパティへのアクセスはコンパイルエラーとはならず、undefined
となります。
type Obj = {str: string, num?: number}
const obj: Obj = {str: "one"}
console.log(obj.str)// "one"
console.log(obj.num)// undefined
上のコード例では、コンパイルエラーが発生することなく、obj
に存在しないプロパティnum
へアクセスできています。
Obj
の型をVSCodeなどでカーソルを合わせて確認してみると、以下のようにundefined
とのユニオン型が付加されています。
type Obj = {
str: string;
num?: number | undefined;
}
このプロパティは一見 num: number | undefined
としても同じに思えますが、そのプロパティが存在しないことが許されるかという違いがあります。
ユニオン型
ユニオン型は「または」を表す型の記法で、|
を用いて T | U
のように記述します。T | U
は"T
型またはU
型"である型を表します。
type T = {str: string, num: number}
type U = {str: string, bool: boolean}
const obj: T | U = {str: "one", num: 1}
const obj2: T | U = {num: 1, bool: true}//T型でもU型でもない場合にはコンパイルエラーとなる。
//Type '{ num: number; bool: true; }' is not assignable to type 'T | U'.
//Property 'str' is missing in type '{ num: number; bool: true; }' but required in type 'U'.(2322)
undefined
とのユニオン型とオプショナルプロパティ
undefined
とのユニオン型T | undefined
は"T型またはundefined
型"である型を表します。オブジェクトのプロパティにこの型がついている場合、このプロパティはundefined
であっても問題ありませんが、プロパティが存在しないことによってundefined
が割り当てられる場合にはコンパイルエラーとなります。
type T = { str?: string , num: number}
type U = { str: string | undefined , num:number}
const tObj: T = { num: 1}//tObjについてはコンパイルエラーが出ない。
const uObj: U = { num: 1}
//Property 'str' is missing in type '{ num: number; }' but required in type 'U'.(2741)
const uObj2: U = { num: 1, str: undefined} //undefinedを割り当てればコンパイルエラーとはならない。
上のコード例からもわかるように、オプショナルプロパティについてはオブジェクトに存在していなくてもエラーとなりませんが、undefined
とのユニオン型の場合にはプロパティが存在しない場合にエラーとなります。
undefined
であってもよいが、オブジェクトに必ず存在していてほしいようなプロパティがあるときには、undefined
とのユニオン型の出番ですね。
12/15 リテラル型と型推論のwidening
リテラル型
string
型やnumber
型などの型はプリミティブ型と呼ばれますが、このプリミティブ型よりもさらに細かい型として、プリミティブ型の部分型であるリテラル型があります。'foo'
や123
などがリテラル型にあたり、この型を持つ変数にはリテラル型として指定した値のみが代入可能となります。
type T = 'octopus'
const str: T = 'octopus'
//Type '"squid"' is not assignable to type '"octopus"'.(2322)
const str2: T = 'squid'
上の例は、リテラル型であるoctopus
型の変数にoctopus
以外の文字列が代入できない例となっています。
プリミティブ型であるboolean
型にも、その部分型にtrue
型とfalse
型というリテラル型が存在します。boolean
型はリテラル型のユニオン型true | false
と同じ型となっています。
const a :boolean = true
//const b: boolean と推論される
const b :true | false = true
テンプレートリテラル型
文字列のリテラル型にはテンプレートリテラル型というものもあり、文字列が特定の形をしていることを明示することができます。文字列のテンプレートと同様に、`${}`
という書き方でテンプレートリテラル型は記述されます。
type Cephalopod = 'octopus' | 'squid'
type T = `${string} is not fish`
//type U = "octopus is not fish" | "squid is not fish"
type U = `${Cephalopod} is not fish`
const tStr: T = 'tuna is not fish'
//Type '"tuna is not fish"' is not assignable to type '"octopus is not fish" | "squid is not fish"'.(2322)
const uStr: U = 'tuna is not fish'
リテラル型のwidening
リテラル型に型推論が働く場合に、推論結果がプリミティブ型へ拡張される場合があります。この働きはリテラル型のwideningと呼ばれています。プリミティブ型へ拡張されるのは、リテラル型で指定した値以外が再代入されることが想定されるものが対象となっています(たとえばlet
変数やオブジェクトリテラルなどが該当します)。
//const constStr: "octopus"
const constStr = 'octopus'
//let letStr: string
let letStr = 'octopus'
wideningが働くのは型推論のときですので、あらかじめ型注釈やas const
をつけておけばwideningされずにリテラル型を使用することが可能です。
type T = 'octopus'
//let letStr: "octopus"
let letStr:T = 'octopus'
//let letStr2: "octopus"
let letStr2 = 'octopus' as const
12/16 どっちのtypeof?
12/17 オブジェクト型を支えるlookup型とkeyof型
12/18 ユーザー定義型ガード
型ガード
型ガードとはif(typeof value === 'string')
のように型による条件分岐やin
演算子などによってブロック内の型を絞り込む機能を指します。この絞り込み部分をユーザー(実装者)が関数の形で作成して、型ガードとして利用するのがユーザー定義型ガードです。
ユーザー定義型ガード
ユーザー定義型ガードとは、条件を満たした時に関数の引数の型をユーザーが設定した型としてTypeScriptのコンパイラに扱わせる機能です。
as
による型アサーションやany
型と同様にTypeScriptの型安全性を破壊しうる危険な機能ですが、確認すべき範囲がas
やany
に比べて明確であることから、これらの機能をやむを得ず使用する場合にはユーザー定義型ガードの使用が推奨されています。
引数 is 型
によるユーザー定義型ガード
関数によって型の絞り込みを行いたい場合には、ユーザー定義型ガードが使用できます。とくに、unknown
型の値を扱うのに便利な機能となっています。
function isNumberOrString(value: unknown){
return typeof value === 'number' || typeof value === 'string'
}
const value: unknown = "314"
if(isNumberOrString(value)){
//'value' is of type 'unknown'.(18046)
console.log(value.toString())
}
上のコード例のように関数による型の条件分岐を行なっても、TypeScriptのコンパイラは型の絞り込みを認識できず、コンパイルエラーとなってしまいます。
そこで、関数の返り値の型にvalue is string | number
とつけることで、関数がtrue
を返した場合にはコンパイラにvalue
はstring | number
型であると認識させることができます。ここの実装を間違えると誤った型をコンパイラが認識したままの状態になり、型安全性が損なわれるので注意が必要です。
function isNumberOrString(value: unknown): value is string | number {
return typeof value === 'number' || typeof value === 'string'
}
const value: unknown = "314"
if(isNumberOrString(value)){
//const value: string | number
console.log(value.toString())// 314
}
asserts 引数 is 型
によるユーザー定義型ガード
関数が例外を投げて終了しない可能性がある場合には、asserts 引数 is 型
の構文によるユーザー定義型ガードが使えます。
こちらは関数がtrue
を返した場合に型を強制するのではなく、関数が最後の処理まで到達した場合に型を強制します。
function isNumber(value: unknown): asserts value is number {
if(typeof value !== 'number'){
throw new Error()
}
return;
}
const value: unknown = 314
try{
// const value: unknown
console.log(value)// 314
isNumber(value)
//const value: number
console.log(value.toString(16))// "13a"
}catch(error){
console.log('error occurred')
}
try
ブロック内ではisNumber
以降の箇所でのvalue
の型がnumber
型として扱われています。
ユーザーが「isNumber
が正常終了しているのならばvalue
の型はnumber
型である」と保証しているわけですが、ここの保証の部分、つまりユーザー定義型ガードの実装に誤りがあると型安全性は大きく損なわれてしまうため、慎重に使用する必要があります。
any
や型アサーションに比べると、ユーザーが責任を負うべき箇所が明確なので、やむを得ない場合にはユーザー定義型ガードの使用を先に検討することが推奨されているわけですね。
12/19 Mapped Typesの基礎
Mapped Typesはユニオン型とプロパティ値の型を指定することで、ユニオン型を構成する各要素をキーとして、指定した値の型をもつオブジェクト型を設定できる機能で、{[P in T]: U}
と言う構文で表されます。
インデックスシグネチャ{[key: K]: U}
に似た構文を持ちますが、プロパティのキーがT
の部分型であるという制約が付いている点が異なります。
インデックスシグネチャにはプロパティ名を動的に決めた場合に存在しないプロパティへのアクセスがコンパイルエラーとならない、という型安全性への問題がありますが、Mapped Typesにはそのような問題がありません。
in演算子
{[P in T]: U}
に登場するin T
部分で使用できる型はstring | number | symbol
の部分型である必要があります。
string | number | symbol
、はkeyof
が取りうるあらゆる型を部分型にもつ、いわゆるkeyof
の上界に当たる型です。
type A = {num: number, str: string}
//Type 'A' is not assignable to type 'string | number | symbol'.(2322)
type B = {[num in A]:number}
// type C = {
// num: number;
// str: number;
// }
type C = {[num in keyof A]:number}
{[P in T]: U}
の例
type T = {
num: number
str: string
bool: boolean
}
// type Obj = {
// num: "num"[];
// str: "str"[];
// bool: "bool"[];
// }
type Obj = {
[P in keyof T]: P[]
}
// type CopyOfT = {
// num: number;
// str: string;
// bool: boolean;
// }
type CopyOfT = {
[P in keyof T]: T[P]
}
Obj
型はkeyof T
、つまり"num" | "str" | "bool"
というユニオン型について、「各構成要素をキー名に持ち、値がそれぞれその要素の配列型P[]
となるプロパティ」をもつオブジェクト型となります。
mapped とは?
mapは数学の用語では写像となります。
写像とは、集合の各要素について、対応する要素を決める規則のことです。
Mapped Typesはx|y|z
という集合の各要素x
、y
、z
を{x:f(x), y:f(y), z:f(z)}
に写像
12/20 Conditional Typesの基礎
Conditional Typesは型の条件分岐を行える機能で、T extends U ? A : B
という構文で表されます。三項演算子と同様に、T
型がU
型の部分型であればA
型に、そうでなければB
型になります。
type IsString<T> = T extends string ? true : false
//type NumIsString = false
type NumIsString = IsString<number>
//type LiteralIsString = true
type LiteralIsString = IsString<'str'>
ユニオン型と分配法則
Conditional Typesの構文T extends U ? A : B
のT
の部分にユニオン型が指定された場合には、分配が行われる点には注意が必要です。
type ToArray<T> = T extends string ? T[] : never
//type UnionArray = "octopus"[] | "squid"[]
type UnionArray = ToArray<"octopus"|"squid">
UnionArray
の型としては、構文を文字通りみていくと"(octopus"|"squid")[]
という型になりそうですが、実際にはユニオン型の構成要素が分配される形となり、"octopus"[] | "squid"[]
型となります。
"(octopus"|"squid")[]
のようなユニオン型の配列型は、以下のコード例のような記述で取得可能です。
type X<T> = T extends string ? [T] : never
//type Y = ("octopus" | "squid")[]
type Y = X<"octopus" | "squid" >[number][]
12/21 PickやPartialなどのユーティリティ型はどのように作られているか?
12/22 Mapped Typesを集合と写像の観点から捉える
12/23 Homomorphic Mapped Types と準同型写像
Mapped Types{[P in T]: U}
のうち、T
の部分がオブジェクトのキーとなっている形{[P in keyof T]: U}
のものはHomomorphic Mappd Types と呼ばれます。
構造を保存する写像である、準同型写像(homomorphic mapping)が名前の由来となっています。
Homomorphic Mapped Types{[P in keyof T]: U}
は、作成元となるオブジェクト型T
と全く同じプロパティキーをもつため、オブジェクトの構造が保存されていることを表しているようですね。
A mapped type of the form { [P in keyof T]: X } is homomorphic with T (because it has the same set of properties as T) and now preserves the optional and readonly modifiers as they exist on the properties in T.
Homomorphic Mapped Typesとプロパティ修飾子の保存
オブジェクト型のプロパティにはreadonly
や?
といったプロパティ修飾子(property modifier)を指定できます。
Homomorphic Mapped Typesにはこの修飾子を保存する機能が備わっています。
type Obj = {
readonly num: number
str ?: string
}
type Homomorphic = {
[PropertyKey in keyof Obj]: Obj[PropertyKey]
}
// type Homomorphic = {
// readonly num: number;
// str?: string | undefined;
// }
type keyStr = "num" | "str"
type NonHomomorphic = {
[PropertyKey in keyStr]: Obj[PropertyKey]
}
// type NonHomomorphic = {
// num: number;
// str: string | undefined;
// }
上のコードでは、Homomorphic Mapped Types を使用した場合には確かに修飾子が保存されていますが、keyof Obj
から得られる型と同じ型"num" | "str"
によって得たMapped Types型を確認してみるとプロパティ修飾子が外れた型となっていることがわかります。
12/24 Record型やReturnType型などのユーティリティ型をみてみる
12/25 inferとはなにものか?Zodを添えて
Advent Calendar 完走できました🎄