🔥

Notion APIをTypeScriptで使ったときのAnyを苦しみながらも撲滅しました

2021/10/31に公開

TypeScript力の低さゆえに、めちゃくちゃ苦しみました。
詳細は↓をご覧ください。

https://github.com/0si43/shetommy.com/pull/24

TypeScriptとAny

TypeScriptでAnyを使うな論は広くあって、たとえば最近見たものだと↓

https://blog.uhy.ooo/entry/2021-10-23/how-you-use-typescript/

などがあります。
言ってることはごもっともで、僕も自分がTypeScript書くまではそう思ってました。

ただ今回、僕はAny撲滅のためにざっくり二週間ぐらいかけています。
(ちゃんと計測してないので、もっとかかってるかもです)
Any撲滅をやっている間は機能追加が止まってしまうので、それだけのメリットがあると言えるのかは疑問でした。

もはやコードの安全性を高めるというよりかは型のためのコードみたいになっていて、コードの可読性はむしろ落ちたところもありました。

いや、型でコンパイル時に静的に検査するメリットというのは重々承知しております。
理想はAnyがない方がいいと思います。
けどなんででしょうね。
普段iOS開発でSwift書いてるときに型で悩んだことってほとんどないんですよね。
なぜこんなWeb開発で型つけようとするとしんどくなるのか、作業しながら疑問でした。

作業前の状況

tsconfig.jsonに"noImplicitAny": falseが指定されていて、型的におかしな記述は// @ts-ignoreをつけていました。
目標はこれを消すことでした。

Notion APIのv0.3.0 -> v0.4.4の注意点

Notion APIですが、v0.3.0以前とそれ以降で型定義が変わっています。

https://github.com/makenotion/notion-sdk-js/issues/200

Changelogのどこにも書いてないですが、だいぶ変わってます。
これがベータ版を使うということなんだな……とちょっと思いました。

PageとかRichTextTextとかがなくなってます。

基本方針

基本的には

  • 基本は型推論で解決
  • 解決できない場合
    • Notion APIのSDKの中で型定義があればそれを使う
    • なければ自分でSDKの中身の型定義を参照して、適宜型をつくる

という方針になるかと思います。
APIからのレスポンスは定義があるので、importするだけです。
api-endpoints.dというところにあります。
(v0.3.0とファイル名も違いますね)

import type {
  QueryDatabaseResponse,
  ListBlockChildrenResponse,
} from '@notionhq/client/build/src/api-endpoints.d'

どうもv0.4.4からの定義はAPIのレスポンス基準で型情報がつくられているみたいです。

TypeScriptでArray要素の型情報にアクセスする

で、v0.3.0以前にはあったPage型なんかは、レスポンスの子プロパティに格下げされています。
型推論が効く場面なら上手いことやってくれますが、適切な粒度でメソッド切ってくとそうもいかないときもあるかと思います。

DBをretriveするAPI叩いたときのレスポンスの中で、Pageに相当する部分はArray形式で入っています。
なので、最初取得できないかと思ったんですが、どうもProperty[number]で型が取れました

declare type NotionPage = QueryDatabaseResponse['results'][number]
declare type NotionProperty =
  QueryDatabaseResponse['results'][number]['properties']

これができたので、色々捗りました。

キツかったところ

二つあります。

コピペした型定義

リッチテキストの型がネスト深すぎて取るのがめちゃくちゃ大変だったので、もうコピペしました。

type richText = {
  type: 'text'
  text: {
    content: string
    link: {
      url: string
    } | null
  }
  annotations: {
    bold: boolean
    italic: boolean
    strikethrough: boolean
    underline: boolean
    code: boolean
    color:
      | 'default'
      | 'gray'
      | 'brown'
      | 'orange'
      | 'yellow'
      | 'green'
      | 'blue'
      | 'purple'
      | 'pink'
      | 'red'
      | 'gray_background'
      | 'brown_background'
      | 'orange_background'
      | 'yellow_background'
      | 'green_background'
      | 'blue_background'
      | 'purple_background'
      | 'pink_background'
      | 'red_background'
  }
  plain_text: string
  href: string | null
}

本当はPickとかを使うとシュッと取れそうなんですが、なんかもういいかと思ってしまいました。

https://qiita.com/k-penguin-sato/items/e2791d7a57e96f6144e5#picktk

Union型とリテラル型の合わせ技が上手く扱えない

二つ目はこちらです。

export const renderBlock = (block: blockWithChildren) => {
  switch (type) {
    case 'paragraph':
      return (
        <p>
          <TextComponent richTexts={block.paragraph.text as richText[]} />
        </p>
      )

Notion APIを使うのであれば絶対書く処理だと思うんですが、NotionのブロックをHTML要素にレンダリングしてます。
例えばNotionの中でparagraphタイプのブロックは<p></p>として書きます。

で、何が問題なのかというと。
blockの中はこんな形式になっております。

{
  "type": "paragraph",
  //...other keys excluded
  "paragraph": {
    "text": [{
      "type": "text",
      "text": {
        "content": "Lacinato kale",
        "link": null
      }
    }],
    "children":[{
      "type": "paragraph"
      // ..other keys excluded
    }]
  }
}

typeはリテラルのユニオン型となっており、"paragraph" | "heading_1" | ……というのが定義です。
で、リテラル型のプロパティを持っておりまして、そこに必要なデータが入っています。
もしAnyが使えるなら、こう書けます。

const renderBlock = (block) => {
  const { type, id } = block
  const value = block[type]

  switch (type) {
    case 'paragraph':
      return (
        <p>
          <Text text={value.text} />
        </p>
      )
    case 'heading_1':
      return (
        <h1>
          <Text text={value.text} />
        </h1>
      )

ところがですね、Anyを不許可にすると、このコードが通りません。
textプロパティを持っていないブロックタイプがあるからですね。
そのため当初に比べると、冗長な書き方で書き直すことになりました。

ダックタイピングみたいな書き方できるといいんですが……

まとめ

というわけでだいぶ血を流しながら、Anyを撲滅した記録でした。
もうちょっと細かいことは↓のスクラップに書いているので、同じような状況にある人は参考にしてみてください。

https://zenn.dev/st43/scraps/6c193bf082af7e

Discussion