なるべく怠けるAPI周りの型付け on next.js
管理画面や生存期間の短い試験機能など「あんまり頑張りたくないけどanyを多少減らす程度には型ほしい」みたいな時などにちょこちょこ使ってるテクニックが溜まってきたのでまとめてみる。
今回はnext.jsのAPIに絞って記述しているが、おそらく他でも使えるはず。
Awaited<ReturnType typeof someFunction>
で怠ける
レスポンスの返り値をAPIのレスポンスが定まり切らなかったり流動的な部分で動的にする場合、Awaited<ReturnType typeof ...>
の組み合わせが便利。
// こんな関数があるとして
// const getFooData = async (id): FooData
// const getBazData = async (id): BazData
// 型を取れるようにレスポンスの関数を切り出し
const someComplexResponse = async (id) => {
const foo = await getFooData(id)
const baz = await getBazData(id)
return { foo, baz }
}
export type SampleApiResponse = Awaited<ReturnType<typeof someComplexResponse>>
// => { foo: FooData, baz: BazData }
const handler: NextApiHandler<SampleApiResponse> = async (req, res) => {
const response = await someComplexResponse(req.query.id)
res.json(response)
}
export default handler
export const useSomeSampleApi = () => {
const { data, error } = useSWR<SampleApiResponse>("/api/sample?id=foo", fetcher)
// dataにSampleApiResponseの型がつく
}
z.infer
でちょっとだけ怠ける
POSTの受け取りデータはzodのPOSTなどでの受け取りはある程度ちゃんとやるしか無いが、その中でも個人的にはzod
でz.infer
するのが一番ラクに感じた
import { NextApiHandler } from "next"
import { z } from "zod"
// スキーマを定義
const AnimalPostScheme = z.object({
name: z.string(),
age: z.number(),
kinds: z.enum(["dog", "cat"])
})
//`z.infer`で型を取り出せる
export type AnimalPostRequest = z.infer<typeof AnimalPostScheme>
const handler: NextApiHandler = async (req, res) => {
try {
const data = AnimalPostScheme.parse(req.body)
const animal = await createAnimal(data)
res.json({ animal })
} catch (e) {
res.status(400).end()
}
}
export default handler
データ作成に利用した値をそのまま取り出せるのであればこういうことになるだろう
const handler: NextApiHandler<AnimalPostRequest> = async (req, res) => {
const animal = await getAnimal(req.query.id)
res.json(animal)
}
string|string[]
を[value].flat(1)
で怠ける
クエリパラメータの最後にクエリパラメータの処理。
これはいくらか乱暴なので使用箇所には注意。
next.jsのAPIのクエリパラメータはstring|string[]
と見分ける必要がある
通常は下記のようにtypeof
にするのが順当だ。
不特定に公開している場合やユーザーからの値を受け入れる場合はもちろん上記のように適切に処理するべきだろう
const handler: NextApiHandler = async (req, res) => {
const id = req.query.id
const targets = req.query.targets
if (typeof id !== "string") {
res.status(400).end()
return
}
if (Array.isArray(targets)) {
res.status(400).end()
return
}
...
一方管理画面などアクセスを制限でき、更にDynamic Routingによって(/api/books/[id].ts
,/api/books/[[...idList]].ts
)string
かstring[]
かがほぼ変わらないようなケースならもう少し手を抜きたい。
string
にしたい場合はtoString
やtemplate litralを利用する手抜きが考えられるだろう
const handler: NextApiHandler = async (req, res) => {
const book = await getBook(req.query.id.toString())
// ...
const handler: NextApiHandler = async (req, res) => {
const book = await getBook(`${req.query.id}`)
// ...
ただfoo,baz
などjoin
した挙動になってしまうのはちょっと不安が残る。
そこで一度配列にしてしまって取り出す
const handler: NextApiHandler = async (req, res) => {
const [id] = [req.query.foo].flat(1)
const book = await getBook(id)
// ...
これであれば配列の先頭が来るので、多少安心感がある
string[]
に寄せたい場合も同様配列化してflat
する手法が使える
const handler: NextApiHandler = async (req, res) => {
const targets = [req.query.targets].flat(1)
const items = await getItems(targets)
// ...
Discussion
クエリパラメータの処理で
flat
、なるほどなぁ。積極的に楽してきたい。