🐕
実務で使えるTypescriptの型8選
ひとこと
実務に組み込みやすいようにできる限り実例を用いました。react前提です。TS初学者の方の助けになれば
解説
テンプレートリテラル型
型安全な文字列操作を実現
例
リテラル型で文字列を定義すれば渡した値がnumberであることを型で保証できる
const requestExternalApi = async ({
data,
}: {
data: {
offset: `${number}`
limit: `${number}`
}
}) => {
const res = await fetch("https://example.com", {
method: "POST",
body: JSON.stringify(data),
})
return res.json()
}
requestExternalApi({
data: {
offset: `${0}`, // `zero`を渡すと型エラーになる
limit: `${10}`, // 同じく`ten`を渡すと型エラーになる
},
})
ジェネリック
例
import { useState } from "react"
const Select = <T extends string,>({
options,
value,
onChange,
}: {
options: T[]
value: T
onChange: (value: T) => void
}) => {
return (
<select value={value} onChange={(e) => onChange(e.target.value as T)}>
{options.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
)
}
type Drink = "water" | "coffee"
const options: Drink[] = ["water", "coffee"]
const Component = () => {
const [selectedOption, setSelectedOption] = useState<Drink>(options[0])
return (
<Select
options={options}
value={selectedOption}
onChange={(val) => setSelectedOption(val)}
/>
)
}
labelとvalueを分けるならこんな感じです。
import { useState } from "react"
const Select = <T extends string, U extends { label: string; value: T }>({
options,
value,
onChange,
}: {
options: U[]
value: T
onChange: (value: T) => void
}) => {
return (
<select value={value} onChange={(e) => onChange(e.target.value as T)}>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
)
}
type Drink = "water" | "coffee"
const userOptions: { value: Drink; label: string }[] = [
{ value: "water", label: "水" },
{ value: "coffee", label: "コーヒー" },
]
const Component = () => {
const [selectedOption, setSelectedOption] = useState(userOptions[0].value)
return (
<Select
options={userOptions}
value={selectedOption}
onChange={(val) => setSelectedOption(val)}
/>
)
}
|
) と交差型 (&
)
ユニオン型 (例
type BasicInfo = {
id: number
name: string
}
type PersonalUser = BasicInfo & {
type: "personal"
age: number
}
type CompanyUser = BasicInfo & {
type: "company"
phone: string
}
type User = PersonalUser | CompanyUser
// ユーザーデータを取得し、混合した状態で返す関数
const fetchUsers = async (): Promise<User[]> => {
const response = await fetch("https://api.example.com/users")
if (!response.ok) {
throw new Error("Failed to fetch users")
}
return (await response.json()) as User[]
}
const Component = async () => {
const users = await fetchUsers()
return (
<div>
{users.map((user) => (
<div>
<div>{user.name}</div>
{user.type === "personal" && <div>{user.age}</div>}
{user.type === "company" && <div>{user.phone}</div>}
</div>
))}
</div>
)
}
Mapped Types
と as
プロパティ名を柔軟に変換できる
例
// KebabCaseはtype-festからインポートすると仮定
import type { KebabCase } from "type-fest"
// 既存の型
type GameSearchState = {
gameId: number
nameQuery: string
isSoldOut: boolean
}
type UrlTypes<T> = {
[K in keyof T as KebabCase<K>]: string
}
type UrlTypeGameSearchState = UrlTypes<GameSearchState>
// 使用例
const search = (state: UrlTypeGameSearchState) => {
const url = new URL("https://example.com/search");
Object.entries(state).forEach(([key, value]) => {
url.searchParams.set(key, value);
});
return (window.location.href = url.toString());
};
const Component = () => {
const [state, setState] = useState<GameSearchState>({
gameId: 1,
nameQuery: "Alice",
isSoldOut: true,
});
const handleSearch = () => {
search({
"game-id": state.gameId.toString(),
"name-query": state.nameQuery,
"is-sold-out": state.isSoldOut.toString(),
});
};
// ...
};
T extends U ? X : Y
)
条件付き型 (条件に応じた型の分岐。型パズルで頻出する
例
const extractNumFields = <T extends Record<string, string | number>>(
response: T,
): { [K in keyof T as T[K] extends number ? K : never]: T[K] } => {
// ...
}
const aaa = extractNumFields({ a: 1, b: "2" }) // => { a: number }
ReturnType
と Parameters
関数の戻り値やパラメータ型を再定義無しで使える
例
const requestToExternalApi = async () => {
const response = await fetch("https://api.example.com/users")
if (!response.ok) {
throw new Error("Failed to fetch users")
}
return response.json() as Promise<{
id: number
type: "user" // バージョンアップ時にフィールド名がvariantに変更されるかもしれない。
}>
}
const convertFromExternalApi = (
// ReturnTypeなので、バージョンアップ時にフィールド名が変更されても型については対応不要
response: Awaited<ReturnType<typeof requestToExternalApi>>, // => { id: number; type: "user" }
) => {
return {
id: response.id,
myType: response.type,
}
}
import { useMemo } from "react"
const useCalculateTotal = (items: { price: number; quantity: number }[]) => {
return items.reduce((total, item) => total + item.price * item.quantity, 0)
}
const Component = ({
itemData1,
itemData2,
}: {
itemData1: { id: string; record: { price: number; quantity: number }[] }
itemData2: { id: string; record: { price: number; quantity: number }[] }
}) => {
const calculatedItems: Parameters<typeof useCalculateTotal>[0] =
useMemo(() => {
return [
...itemData1.record.map((item) => ({
price: item.price,
quantity: item.quantity,
})),
...itemData2.record.map((item) => ({
price: item.price,
quantity: item.quantity,
})),
]
}, [itemData1.record])
const total = useCalculateTotal(calculatedItems)
return <div>合計金額: {total}円</div>
}
as const
とsatisfies
as constとsatisfiesを使って、リテラル型を維持しつつもsatisfiesで型を満たすことを保証。
例
const localizeData_JP = {
novel: {
chapter: "チャプター",
episode: "話",
},
} as const satisfies {
novel: {
chapter: string
episode: string
}
}
const chapter = localizeData_JP.novel.chapter // => "チャプター"
再帰的
再帰的データ構造の解析は読みづらいことが多い
例
localizeの引数に.で繋がった文字列を渡す場合に間違った文字列の場合型エラーを発生させる
const localizeData_JP = {
novel: {
chapter: "チャプター",
episode: "話",
},
} as const satisfies {
novel: {
chapter: string
episode: string
}
}
type RecursiveObj = { [key: string]: string | RecursiveObj }
type UnionWithDot<
Prefix extends string,
Key,
> = `${Prefix}${Prefix extends "" ? "" : "."}${Key extends string ? Key : ""}`
type DotKeys<Obj extends RecursiveObj, Prefix extends string = ""> = {
[Key in keyof Obj]: Obj[Key] extends object
? DotKeys<Obj[Key], UnionWithDot<Prefix, Key>>
: UnionWithDot<Prefix, Key>
}[keyof Obj]
export type ExtractedDotKeys = DotKeys<typeof localizeData_JP>
const localize = (key: ExtractedDotKeys) => {
const splittedKey = key.split(".")
const stringOrObj = splittedKey.reduce<RecursiveObj | string>((obj, key) => {
if (typeof obj === "string") return obj
return obj[key]
}, localizeData_JP)
return stringOrObj
}
localize("novel.chapter") // novel.chapにすると型エラーになる
まとめ
型とは向き合い続けるしか。
フィシルコムのテックブログです。マーケティングSaaSを開発しています。 マイクロサービス・AWS・NextJS・Golang・GraphQLに関する発信が多めです。 カジュアル面談はこちら(ficilcom.notion.site/bbceed45c3e8471691ee4076250cd4b1)から
Discussion