Pickの使いどころ
Pickについて考えたので書く
最小サンプル
type Product = {
name: string
price: number
brand: {
id: number,
name: string
}
}
//type Pick<T, K extends keyof T>
type PickedProduct = Pick<Product,"name"|"brand">
PickedProduct は以下の型として展開される
{
name:string
brand: {
id:number
name:string
}
}
つまりTの型から K に指定したキーだけの部分型を切り出せる。
サンプルコード
実際単独でPickを使うことは個人的にあまり無く、ジェネリクスとの併用で使う事が多いんじゃ無いかと思います。
よくある商品APIを例にして実用例を説明します。
例えば /product/:id をリクエストすることで 商品(Product)を返すAPIでクエリパラメーターで
レスポンスに含まれるプロパティを指定できるものがあるとします。
//
/*
* https://localhost/product/1
* クエリパラメーターを指定しない場合は全プロパティを返す
*/
// 返却レスポンス
{
name : "令和最新 第3世代 bluetoothイヤホン",
price : 3400,
brand : {
id: 234,
name: "M.I.C"
}
}
//
/*
* https://localhost/product/1?fields[]=name&fields[]=brand
* クエリパラメーターを指定した場合は
*/
// 返却レスポンス ※price を指定していないので レスポンスから取り除かれます。
{
name : "令和最新 第3世代 bluetoothイヤホン",
brand : {
id: 234,
name: "M.I.C"
}
}
これをFetchAPI で取得する関数を考えます。
まず単純に書きます。
type Product = {
name: string
price: number
brand: {
id:number
name:
}
}
const getProduct = async( id : number ,fields? :string[] ) => {
const query = (fields)
? "?" + fields.map(field => `fields[]=${field}`).join("&")
: ""
const uri = `https://localhost/product/${id}${query}`
const response = await fetch(uri)
//エラー処理は省略
return await response.json() as Product
}
response.json() は Promise<any> になってしまうので as で型をつけてみました。
一見良いですが これでは、fields を指定した際の絞り込まれたレスポンスと異なり実行時エラーが発生します。
// 実行時エラーが発生するコード
;(async() => {
const product = await getProduct(1,["name"])
// product.brand が存在しないため エラーになります。
console.log(product.brand.id)
})()
対処としてレスポンスのキーがあるかどうかわかならないなら省略可能型にしてしまえという発想がまず浮かびます。
type Product = {
name?: string
price?: number
brand?: {
id:number
name:string
}
}
const getProduct = async( id : number ,fields? :string[] ) => {
const query = (fields)
? "?" + fields.map(field => `fields[]=${field}`).join("&")
: ""
const uri = `https://localhost/product/${id}${query}`
const response = await fetch(uri)
//エラー処理は省略
return await response.json() as Product
}
Product は そのままにし await response.json() as Partial<Product> にしても良いでしょう。
実行時は 存在チェックをすればコンパイルエラーになりません。
//実行コード
;(async() => {
const product = await getProduct(1,["name","brand"])
//プロパティにアクセスするときは存在チェックをしてから!
if(product.brand)
console.log(product.brand.id)
}
})()
実際これでも間違いではなく、コンパイルエラーにもならず、実行時も安全なのですが
ただただただ面倒でこういったコードだらけになると確実にTypescriptを嫌いになります。
加えて、fieldsにProductに存在しないkeyが指定できることにも問題があります。
PickとGenericsを併用して書く
こういった場合はジェネリクス(引数からの推論)とPickを使う事で表現できます。
以下のように書き換えます。 やっとPickが出てきます。
type Product = {
name: string
price: number
brand: {
id:number
name:string
}
}
const getProduct = async<T extends keyof Product>( id : number ,fields? : T[] ) => {
const query = (fields)
? "?" + fields.map(field => `fields[]=${field}`).join("&")
: ""
const uri = `https://localhost/product/${id}${query}`
const response = await fetch(uri)
//エラー処理は省略
return await response.json() as Pick<Product,T>
}
- T extends keyof Product は 型T が Productに存在するキーである事を強制します。
- fields? :T[] の箇所の記述で型Tは型引数で明示しない場合はfiledsに指定された引数から推論されます。(つまり T は fieldsに指定された要素のUnion型を取る)
- Pick<Product,T> でレスポンス絞り込みます。
使用感
;(async() => {
const product = await getProduct(1)
/*
product は 以下の型とみなされる
{
name: string
price: number
brand: {
id:number
name:string
}
}
*/
const product2 = await getProduct(2,["name"])
/*
product2 は 以下の型とみなされる
{
name: string
}
*/
const product3 = await getProduct(3,["name","brand"])
/*
product3 は 以下の型とみなされる
{
name: string
brand: {
id:number
name:string
}
}
*/
})()