Closed45

個人ブログにURLのOG画像カードつける

蔀

とりあえずパッケージ入れてみたら、Module not found: Can't resolve 'util/types'
その他パッケージを最新化してみる

node -v 
npm install -g n   
n latest
node -v

https://blog.hubspot.com/website/update-node-js

蔀

node.js、どうやらバージョン管理ソフト入れずにhomebrewで直インストールだったらしい
絶対バージョン管理入れた方がいいけど踏襲

 1069  brew list
 1070  brew upgrade
 1071  node -v
 1072  npm run dev
 1073  npm run dev
 1074  npm outdaed
 1075  npm outdated
 1076  npm uninstall @notionhq/client
 1077  npm install @notionhq/client
 1078  npm uninstall react 
 1079  npm install react 
 1080  npm uninstall react-dom 
 1081  npm install react-dom 
 1082  npm run dev

蔀

Notionクライアントはそのままにしたいのでバージョン指定
これも更新したい……

npm install @notionhq/client@0.4.4
蔀

Caludeにコード吐いてもらってテキトーにやってたら、
open-graph-scraperが内部で使ってるundiciのモジュールをwebpackが上手くロードできなくてエラーになった

./node_modules/open-graph-scraper/node_modules/undici/lib/web/fetch/util.js
Module parse failed: Unexpected token (874:57)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
|       // 5. If object is not a default iterator object for interface,
|       //    then throw a TypeError.
>       if (typeof this !== 'object' || this === null || !(#target in this)) {
|         throw new TypeError(
|           `'next' called on an object that does not implement interface ${name} Iterator.`
蔀

Next.jsのバージョンを雑に14に上げたのが影響かなと思ったけど、12に戻しても同じだった
open-graph-scraperを使おうとすると発生するっぽい

蔀

Package.jsonを

"open-graph-scraper": "~6.0.0"

にしたら動く?

蔀

やっと動くところまで行ったけど、やりたかったことでは全然ないな

蔀

取得できたOGPデータ

{
  ogTitle: '2023年上半期ふりかえり: 一番のヘタクソでいよう、その先へ',
  ogType: 'article',
  ogDescription: 'Written by 蔀',
  twitterCard: 'summary_large_image',
  ogImage: [
    {
      url: 'https://www.shetommy.com/_next/image?url=%2Fprofile.png&w=640&q=75'
    }
  ],
  requestUrl: 'https://www.shetommy.com/articles/2023%E5%B9%B4%E4%B8%8A%E5%8D%8A%E6%9C%9F%E3%81%B5%E3%82%8A%E3%81%8B%E3%81%88%E3%82%8A:%20%E4%B8%80%E7%95%AA%E3%81%AE%E3%83%98%E3%82%BF%E3%82%AF%E3%82%BD%E3%81%A7%E3%81%84%E3%82%88%E3%81%86%E3%80%81%E3%81%9D%E3%81%AE%E5%85%88%E3%81%B8',
  success: true
}
蔀

paragraphブロック、リッチテキスト要素が配列として入ってきている
これは通常であれば一つだが、文中リンクがあった場合に配列として入ってくる模様

[
  {
    type: 'text',
    text: {
      content: '個人開発に関してもちょっと悩みがあって、人の役に立つものを何かつくりたいと思っているが、特に思いつかないので、結局',
      link: null
    },
    annotations: {
      bold: false,
      italic: false,
      strikethrough: false,
      underline: false,
      code: false,
      color: 'default'
    },
    plain_text: '個人開発に関してもちょっと悩みがあって、人の役に立つものを何かつくりたいと思っているが、特に思いつかないので、結局',
    href: null
  },
  {
    type: 'text',
    text: { content: 'PoP(Pieces of Paper)', link: [Object] },
    annotations: {
      bold: false,
      italic: false,
      strikethrough: false,
      underline: false,
      code: false,
      color: 'default'
    },
    plain_text: 'PoP(Pieces of Paper)',
    href: 'https://github.com/0si43/PiecesOfPaper'
  },
  {
    type: 'text',
    text: { content: 'と', link: null },
    annotations: {
      bold: false,
      italic: false,
      strikethrough: false,
      underline: false,
      code: false,
      color: 'default'
    },
    plain_text: 'と',
    href: null
  },
  {
    type: 'text',
    text: { content: 'shetommy.com', link: [Object] },
    annotations: {
      bold: false,
      italic: false,
      strikethrough: false,
      underline: false,
      code: false,
      color: 'default'
    },
    plain_text: 'shetommy.com',
    href: 'https://github.com/0si43/shetommy.com'
  },
  {
    type: 'text',
    text: {
      content: '以降何もつくっていない。PoPを地道に改善していくのも粛々とやっていくが、ぼちぼちこのアプリも俺の中で役目を終えつつある感じもして、次に行きたい。が、アイディアがない。',
      link: null
    },
    annotations: {
      bold: false,
      italic: false,
      strikethrough: false,
      underline: false,
      code: false,
      color: 'default'
    },
    plain_text: '以降何もつくっていない。PoPを地道に改善していくのも粛々とやっていくが、ぼちぼちこのアプリも俺の中で役目を終えつつある感じもして、次に行きたい。が、アイディアがない。',
    href: null
  }
]
蔀

VSCodeのブレイクポイント使えないと不便なのでClaudeに設定方法を聞いた

蔀

VSCodeでは、Node.jsアプリケーションのデバッグ機能が組み込まれています。npm run devで起動したアプリケーションにブレイクポイントを設定するには、以下の手順を踏みます。

  1. プロジェクトのルートディレクトリに .vscode フォルダを作成します(まだない場合)。

  2. .vscode フォルダ内に launch.json ファイルを作成します。

  3. launch.json に以下の設定を追加します:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug npm script",
      "runtimeExecutable": "npm",
      "runtimeArgs": [
        "run-script",
        "dev"
      ],
      "port": 9229
    }
  ]
}
  1. VSCodeのデバッグビューを開きます(サイドバーの虫のアイコン、またはCtrl + Shift + D)。

  2. 上部の実行ボタンの隣にあるドロップダウンメニューから "Debug npm script" を選択します。

  3. ブレイクポイントを設定したい行の左側の余白をクリックして、ブレイクポイントを設定します。

  4. デバッグビューの上部にある再生ボタン(緑の三角形)をクリックしてデバッグを開始します。

  5. アプリケーションが起動し、設定したブレイクポイントで停止します。

これで、npm run devで起動したアプリケーションにブレイクポイントを設定し、デバッグを行うことができます。
デバッグコンソールやウォッチ式を使って変数の値を確認したり、ステップ実行によりコードの流れを追ったりすることができます。

注意:この方法では、アプリケーションがデバッグモードで起動されるため、パフォーマンスが通常より低下する可能性があります。
本番環境ではデバッグモードを無効にすることをお勧めします。

蔀

はい、VSCodeのデバッグ中にconsole.logなどのコマンドを使うことができます。デバッグコンソール(Debug Console)を使って、現在のスコープで有効な JavaScript コードを実行できます。

デバッグ中の便利な機能として:

  1. ウォッチ式(Watch Expressions):監視したい変数や式を追加することで、その値を常に確認できます。

  2. ホバリング:コード内の変数の上にマウスカーソルを置くと、その時点での変数の値がポップアップ表示されます。

  3. デバッグコンソール(Debug Console):console.logの出力を確認したり、JavaScriptコードを実行したりできます。例えば、ブレイクポイントで停止中に変数の値を変更することもできます。

  4. コールスタック(Call Stack):現在の実行位置に至るまでの関数呼び出しの流れを表示します。

  5. ステップ実行:「ステップオーバー(Step Over)」、「ステップイン(Step Into)」、「ステップアウト(Step Out)」の機能を使って、コードを1行ずつ実行できます。

これらの機能を活用することで、コードの動作を詳細に調査し、バグの原因を特定しやすくなります。
console.logは古典的なデバッグ方法ですが、VSCodeの豊富なデバッグ機能と組み合わせることで、より効率的にデバッグを行えるでしょう。

蔀

デバッグコンソールで式を評価した際に、undefinedが出力されるのは正常な動作です。

デバッグコンソールに式を入力すると、その式が評価され、結果が表示されます。しかし、式の評価自体は値を返さないため、undefinedが出力されます。これは、コンソールが式の評価を完了したことを示しています。

例えば、デバッグコンソールで以下のように入力したとします:

console.log("Hello, world!")
myObject

出力結果は以下のようになります:

Hello, world!
undefined
{
  // myObjectの中身
}

最初の行はconsole.logの出力、2行目のundefinedはconsole.log式自体の評価結果、3行目以降はmyObjectの内容が表示されます。

したがって、undefinedが出力されるのは問題ではなく、むしろJavaScriptの式の評価動作を反映した正常な動作だと言えます。オブジェクトの内容が正しく表示されている限り、undefinedが出力されることを気にする必要はありません。

蔀

リンクカードの仕様

方針

  • ブロックが丸ごとリンクを表していたらカード化する
  • 文中リンクのように、一部がリンクだがそれ以外の情報もあるようなケースはそのままにしたい

詳細

ブロックタイプ一覧

  • paragraph
    • リッチテキスト要素が一つで、それに対してURLが指定されていたらカード化する
  • bookmark
    • カード化する
  • link_preview
    • Githubのプルリクとかで使える
    • Notion上のアイキャッチである程度動的に情報取得ができる。リアルタイム性の高いOGPみたいなイメージ
    • 別に必要ない気がするけど、簡単に対応できそうなんでやっとく
蔀

今なら "open-graph-scraper": "^6.4.0" を指定しても動くな

蔀

package.jsonでの依存関係のバージョン指定において、^~の違いは以下の通りです。

  1. ^ (キャレット):

    • ^は、指定したバージョンと互換性のある最新のマイナーバージョンまでアップデートできることを意味します。
    • 例えば、^1.2.3と指定すると、1.2.3以上2.0.0未満の最新バージョンが使用されます。
    • これにより、バグ修正や新機能が含まれる可能性のあるマイナーバージョンアップデートを受け取ることができます。
    • ただし、メジャーバージョンの変更は含まれません。
  2. ~ (チルダ):

    • ~は、指定したバージョンと互換性のある最新のパッチバージョンまでアップデートできることを意味します。
    • 例えば、~1.2.3と指定すると、1.2.3以上1.3.0未満の最新バージョンが使用されます。
    • これにより、バグ修正が含まれるパッチバージョンアップデートのみを受け取ることができます。
    • マイナーバージョンやメジャーバージョンの変更は含まれません。

つまり、^はメジャーバージョンを固定し、マイナーバージョンとパッチバージョンの変更を許容するのに対し、~はメジャーバージョンとマイナーバージョンを固定し、パッチバージョンの変更のみを許容します。

一般的に、^を使用することで、互換性のあるマイナーバージョンアップデートを受け取り、新機能やバグ修正の恩恵を受けることができます。ただし、プロジェクトの安定性を重視する場合や、特定のバージョンに依存している場合は、~を使用してパッチバージョンのみを更新することができます。

最終的には、プロジェクトの要件やリスク許容度に応じて、適切なバージョン指定を選択することが重要です。

蔀

こういう書き方できるなら、もっと色々できるな

export type BlockWithChildren = ListBlockChildrenResponse['results'][number] & {
  children?: BlockWithChildren[],
  ogpData?: OgpData 
}
蔀

参考にしたところのこの型がどこから持ってきたのかよくわからない
動きはするけど

export type OgpData = OpenGraphProperties & {
  ogImage?: OpenGraphImage | OpenGraphImage[] | undefined;
};
蔀

Node.jsモジュールのtypes.d.tsファイルでインポートされている型を直接参照することは、一般的にはできません。これは、TypeScriptの型定義ファイルの制限によるものです。

型定義ファイル(.d.ts)は、コンパイル時に使用される型情報を提供するためのものであり、実行時には存在しません。そのため、types.d.tsファイル内でインポートされた型は、そのファイル内でのみ有効であり、外部からは直接参照できません。

ただし、モジュールがその型を外部に公開している場合は、別の方法で型を参照することができます。以下の方法を試してみてください。

  1. モジュールがエクスポートしている型を使用する:

    import { OgObject } from 'module-name';
    

    モジュールがOgObject型を直接エクスポートしている場合は、上記の方法で型を参照できます。

  2. モジュールの型定義を拡張する:

    declare module 'module-name' {
      export type OgObject = {
        // OgObjectの型定義をここに記述
      };
    }
    

    モジュールの型定義を拡張することで、OgObject型を定義し、モジュールから型をインポートできるようにします。

  3. 型定義ファイルを手動でコピーする:
    モジュールのtypes.d.tsファイルから必要な型定義をコピーし、自分のプロジェクトの型定義ファイルに貼り付けます。ただし、この方法はメンテナンス性が低く、型定義の更新に追従しにくいという欠点があります。

  4. モジュールの型定義ファイルを参照する:
    tsconfig.jsonの"include"オプションに、モジュールの型定義ファイルのパスを追加します。

    {
      "compilerOptions": {
        // ...
      },
      "include": [
        // ...
        "node_modules/module-name/types.d.ts"
      ]
    }
    

    これにより、TypeScriptコンパイラがモジュールの型定義ファイルを認識し、型を参照できるようになります。ただし、この方法はモジュールの実装の詳細に依存するため、注意が必要です。

これらの方法でも型の参照ができない場合は、モジュールの提供する型情報が不十分である可能性があります。その場合は、モジュールのドキュメントを参照するか、モジュールの開発者にお問い合わせください。

一般的に、Node.jsモジュールがTypeScriptで書かれている場合、型定義ファイルを介して型情報を提供するのが一般的です。しかし、すべてのモジュールが適切な型定義を提供しているとは限りません。モジュールの型定義が不十分な場合は、手動で型を定義するか、代替のモジュールを探すことを検討してください。

蔀

import OgObject from 'open-graph-scraper/dist/lib/types.d' のようにNode.jsモジュールの内部の型定義ファイルを直接インポートすることには、いくつかの懸念点があります。

  1. 型定義ファイルの安定性:
    モジュールの内部の型定義ファイルは、モジュールの実装の詳細に依存しています。モジュールのバージョンアップや内部構造の変更により、型定義ファイルのパスや内容が変更される可能性があります。その場合、型定義のインポートが失敗したり、型の不一致が発生したりする可能性があります。

  2. 型定義ファイルの可視性:
    モジュールの内部の型定義ファイルは、一般的に公開されておらず、外部からアクセスすることを想定していない場合があります。モジュールの作者が意図的に型定義を非公開にしている場合、直接アクセスすることは適切ではないかもしれません。

  3. モジュールの更新への追従:
    モジュールがバージョンアップされた場合、内部の型定義ファイルのパスや内容が変更される可能性があります。その場合、型定義のインポートを手動で更新する必要があります。これは、モジュールの更新に追従するのが難しくなる可能性があります。

  4. 型定義の不完全性:
    モジュールの内部の型定義ファイルは、モジュールの実装のために用意されたものであり、公開用の型定義としては不完全である可能性があります。型定義が不完全な場合、型チェックが適切に行われない可能性があります。

これらの懸念点を考慮すると、直接モジュールの内部の型定義ファイルをインポートするのは、一般的には推奨されません。代わりに、以下のような方法を検討してください。

  1. モジュールが提供する公開用の型定義を使用する。
  2. モジュールのドキュメントや公式のサンプルコードを参照し、推奨される型定義の使用方法に従う。
  3. 型定義が不十分な場合は、自分で型定義を作成するか、any型を使用して型チェックを回避する。

ただし、状況によっては、モジュールの内部の型定義ファイルを直接インポートすることが唯一の選択肢である場合もあります。その場合は、モジュールのバージョンを固定し、型定義のインポートが失敗した場合に備えて適切なエラーハンドリングを行うことが重要です。

また、モジュールの作者に、公開用の型定義の提供を要望することも検討してください。これにより、将来的により安定した型定義が利用できるようになるかもしれません。

蔀

import文における{}の有無は、モジュールのエクスポート方法によって決まります。

  1. デフォルトエクスポート(Default Export):

    • モジュールがデフォルトエクスポートを使用している場合、import文で{}を使用する必要はありません。
    • デフォルトエクスポートは、export defaultを使用してエクスポートされます。
    • 例えば、import openGraphScraper from 'open-graph-scraper'のように、{}なしで直接インポートできます。
    • デフォルトエクスポートは、モジュールごとに1つだけ存在できます。
  2. 名前付きエクスポート(Named Export):

    • モジュールが名前付きエクスポートを使用している場合、import文で{}を使用する必要があります。
    • 名前付きエクスポートは、exportキーワードを使用して個別にエクスポートされます。
    • 例えば、import { namedExport1, namedExport2 } from 'module'のように、{}内に必要なエクスポートの名前を指定してインポートします。
    • 名前付きエクスポートは、モジュール内で複数存在できます。

モジュールがデフォルトエクスポートと名前付きエクスポートの両方を持つ場合は、以下のようにインポートできます。

import defaultExport, { namedExport1, namedExport2 } from 'module';

この場合、デフォルトエクスポートはdefaultExportという名前でインポートされ、名前付きエクスポートは{}内で指定された名前でインポートされます。

import openGraphScraper from 'open-graph-scraper'の場合、open-graph-scraperモジュールはデフォルトエクスポートを使用しているため、{}なしでインポートできます。

一方、import { namedExport } from 'module'のように{}を使用する場合は、moduleモジュールが名前付きエクスポートを使用していることを示しています。

モジュールのドキュメンテーションや定義を確認することで、適切なインポート方法を判断できます。

蔀

import文における{}typeの有無により、4つのパターンがあります。それぞれのパターンの意味と使い方を整理します。

  1. import { identifier } from 'module'

    • これは名前付きのインポートです。
    • moduleから特定のidentifier(関数、クラス、変数など)をインポートします。
    • {}内には、インポートしたい識別子を指定します。
    • インポートされた識別子は、そのまま使用できます。
  2. import { identifier as alias } from 'module'

    • これも名前付きのインポートですが、asを使用してエイリアス(別名)を付けています。
    • moduleから特定のidentifierをインポートし、aliasという別名で使用します。
    • エイリアスを使用すると、名前の衝突を回避したり、より意味のある名前を付けたりできます。
  3. import defaultExport from 'module'

    • これはデフォルトのインポートです。
    • moduleのデフォルトエクスポートをdefaultExportという名前でインポートします。
    • {}は使用しません。
    • デフォルトエクスポートは、export defaultを使用してモジュールからエクスポートされます。
    • モジュールごとに1つのデフォルトエクスポートのみが可能です。
  4. import type { TypeName } from 'module'

    • これは型のみのインポートです。
    • moduleから特定の型TypeNameをインポートします。
    • importの前にtypeキーワードを使用することで、値ではなく型のみをインポートすることを明示します。
    • インポートされた型は、型アノテーションやジェネリックスなどで使用できます。
    • 型のみのインポートは、実行時には影響しません。

これらのパターンを組み合わせることもできます。例えば:

import defaultExport, { namedExport1, namedExport2 as alias } from 'module';
import type { TypeName } from 'module';

上記の例では、デフォルトエクスポートと名前付きエクスポートを同時にインポートし、さらに型のみのインポートも行っています。

import文の構文は、モジュールのエクスポート方法と対応している必要があります。モジュールがデフォルトエクスポートと名前付きエクスポートのどちらを使用しているかによって、適切なインポート構文を選択します。また、型のみをインポートする場合は、typeキーワードを使用して明示的に指定します。

蔀

じゃああんま良くなさそうだけど、この指定で行きます

import { type OgObject } from 'open-graph-scraper/dist/lib/types.d'
蔀

既存の型定義も微妙だな
これはNotion APIのクライアントをバージョンアップするときに対応するか

蔀

普通にデプロイのときにビルドエラーで怒られるので対処する
(run devでもビルドエラーになるようにしたいな)

蔀

この型指定に対して

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

このビルドエラーが出ます。どうしたらいいでしょう?

Property 'properties' does not exist on type '{ parent: { type: "database_id"; database_id: string; } | { type: "page_id"; page_id: string; } | { type: "workspace"; workspace: true; }; properties: Record<string, { type: "title"; title: RichTextItemResponse[]; id: string; } | ... 17 more ... | { ...; }>; ... 7 more ...; url: string; } | { ...; } | { ...; } | { ....'.
蔀

ビルドエラーが発生している原因は、QueryDatabaseResponse['results'][number]の型が複数の可能性があるためです。QueryDatabaseResponse['results']の要素には、propertiesプロパティを持つ型と持たない型が混在しています。

このエラーを解決するには、NotionPropertyの型を条件付きで定義する必要があります。以下のように、QueryDatabaseResponse['results'][number]の型をチェックし、propertiesプロパティが存在する場合にのみNotionPropertyの型を定義します。

declare type NotionProperty = QueryDatabaseResponse['results'][number] extends {
  properties: infer P;
}
  ? P
  : never;

この条件付きの型定義では、以下のようなことを行っています:

  1. QueryDatabaseResponse['results'][number]{ properties: ... }の形式を満たすかどうかを確認します。
  2. 条件が満たされる場合、infer Pを使用してpropertiesの型を推論し、NotionPropertyの型として使用します。
  3. 条件が満たされない場合、never型が割り当てられます。これは、propertiesプロパティが存在しない場合にはNotionPropertyの型が定義されないことを意味します。

この修正により、NotionPropertyの型はpropertiesプロパティが存在する場合にのみ定義されるようになります。これにより、ビルドエラーが解消されるはずです。

ただし、この方法ではNotionPropertyの型がneverになる可能性があることに注意してください。実際にpropertiesプロパティが存在しない場合を適切に処理する必要があります。

もしpropertiesプロパティが常に存在することが保証されているのであれば、型アサーションを使用してpropertiesプロパティの存在を明示的に示すこともできます。

declare type NotionProperty = QueryDatabaseResponse['results'][number]['properties'] as Record<
  string,
  any
>;

この場合、propertiesプロパティが存在しない場合にはランタイムエラーが発生する可能性があるため、注意が必要です。

適切な方法は、QueryDatabaseResponseの型定義と実際のデータ構造に依存します。可能であれば、モジュールの作者に型定義の改善を提案することも検討してください。

蔀

なるほど、NotionPropertyが常にnever型になってしまうのは、QueryDatabaseResponse['results'][number]の型がpropertiesプロパティを持たない可能性が高いためだと思われます。

この場合、QueryDatabaseResponse['results']の型を詳細に検査し、propertiesプロパティを持つ型のみを抽出する必要があります。以下のように、条件付きの型とExtractユーティリティ型を使用して、propertiesプロパティを持つ型のみを抽出できます。

type ResultWithProperties = Extract<
  QueryDatabaseResponse['results'][number],
  { properties: Record<string, any> }
>;

declare type NotionProperty = ResultWithProperties['properties'];

ここでは、以下のようなことを行っています:

  1. ResultWithProperties型を定義し、QueryDatabaseResponse['results'][number]の型から{ properties: Record<string, any> }の形式を満たす型のみを抽出します。Extractユーティリティ型は、指定された型のユニオンから、指定された条件を満たす型のみを抽出します。

  2. NotionProperty型をResultWithProperties['properties']として定義します。これにより、propertiesプロパティを持つ型のみがNotionProperty型に割り当てられます。

この修正により、NotionProperty型はpropertiesプロパティを持つ型のみを表すようになります。never型になることはなくなるはずです。

ただし、この方法では、QueryDatabaseResponse['results']の型が正確に定義されていることが前提となります。QueryDatabaseResponseの型定義が不完全な場合や、実際のデータ構造と型定義が一致しない場合は、別の方法を検討する必要があります。

可能であれば、@notionhq/clientモジュールの型定義を確認し、QueryDatabaseResponseの型定義が適切であるかどうかを確認することをお勧めします。型定義が不完全な場合は、モジュールの作者に問題を報告するか、自分で型定義を拡張することを検討してください。

正確な型定義を使用することで、TypeScriptの型安全性を最大限に活用し、開発体験を向上させることができます。

蔀

Extractは、TypeScriptの組み込みユーティリティ型の1つです。Extractは、指定された型のユニオンから、指定された条件を満たす型のみを抽出します。

Extractの構文は次のようになります:

type Extract<T, U> = T extends U ? T : never;
  • Tは、抽出元の型のユニオンです。
  • Uは、抽出条件となる型です。

Extractは、Tの各要素に対して、Uに割り当て可能であるかどうかを確認します。割り当て可能な場合は、その要素の型が抽出されます。割り当て可能でない場合は、never型が割り当てられます。

例えば、以下のような使い方ができます:

type NumberOrString = number | string;
type OnlyNumber = Extract<NumberOrString, number>;
// OnlyNumber は number 型になる

type Animal = 'dog' | 'cat' | 'bird';
type Pet = Extract<Animal, 'dog' | 'cat'>;
// Pet は 'dog' | 'cat' 型になる

type User = { id: number; name: string } | { id: number; email: string };
type UserWithName = Extract<User, { name: string }>;
// UserWithName は { id: number; name: string } 型になる

上記の例では、Extractを使用して、ユニオン型から特定の条件を満たす型のみを抽出しています。

Extractは、型のフィルタリングや条件付きの型の抽出に非常に便利です。特に、複雑なユニオン型から特定のプロパティを持つ型のみを抽出する場合に役立ちます。

NotionPropertyの例では、Extractを使用して、QueryDatabaseResponse['results'][number]の型からpropertiesプロパティを持つ型のみを抽出しています。これにより、NotionProperty型がpropertiesプロパティを持つ型のみを表すようになります。

Extractは、TypeScriptの型システムをより柔軟かつ表現力豊かにするための強力なツールの1つです。適切に使用することで、型の安全性を維持しながら、必要な型のみを抽出することができます。

蔀

block.results[0]の型がPartialBlockObjectResponse | BlockObjectResponseとなっており、PartialBlockObjectResponseにはtypeプロパティが存在しないためエラーが発生しています。

この問題を解決するためには、型ガードを使用してblock.results[0]の型を絞り込む必要があります。具体的には、block.results[0]BlockObjectResponse型であることを確認してから、typeプロパティにアクセスします。

以下のように修正してみてください:

const block = await notion.blocks.children.list({
  start_cursor: cursor,
  block_id: blockId,
  page_size: 1,
});

if (block.results[0] && 'type' in block.results[0]) {
  const blockType = block.results[0].type;
  if (blockType === 'paragraph') {
    block.results[0].paragraph.text.forEach((textObject) => {
      openingSentence += textObject.plain_text;
    });
  }
}

ここでは、以下のように型ガードを使用しています:

  1. block.results[0]が存在することを確認するために、block.results[0]の真偽値をチェックします。これにより、block.resultsが空の場合にエラーが発生することを防ぎます。

  2. 'type' in block.results[0]を使用して、block.results[0]typeプロパティが存在するかどうかを確認します。これにより、block.results[0]BlockObjectResponse型であることを保証します。

  3. block.results[0].typeを別の変数blockTypeに代入します。これにより、TypeScriptはblock.results[0]BlockObjectResponse型であると推論し、typeプロパティへのアクセスが可能になります。

  4. blockTypeを使用して、ブロックのタイプが'paragraph'であるかどうかを判定し、適切な処理を行います。

この修正により、block.results[0]BlockObjectResponse型でない場合やtypeプロパティが存在しない場合でもエラーが発生しなくなります。

型ガードを使用することで、実際のデータ構造に合わせて型を絞り込むことができます。これにより、型エラーを回避しつつ、正しい型情報に基づいてコードを記述できます。

蔀

JavaScriptの substr() メソッドは非推奨(deprecated)となり、代わりに substring() または slice() メソッドの使用が推奨されています。

  1. substring(start, end):

    • start: 開始位置のインデックス(0から始まる)
    • end: 終了位置のインデックス(省略可能)
    • 指定された開始位置から終了位置の直前までの部分文字列を返します。
    • 終了位置を省略した場合、文字列の最後までが対象となります。

    例:

    const str = 'Hello, World!'
    const substr = str.substring(0, 5)
    console.log(substr) // 出力: 'Hello'
    
  2. slice(start, end):

    • start: 開始位置のインデックス(0から始まる)
    • end: 終了位置のインデックス(省略可能)
    • 指定された開始位置から終了位置の直前までの部分文字列を返します。
    • 終了位置を省略した場合、文字列の最後までが対象となります。
    • 負のインデックスを指定すると、文字列の末尾からの位置として扱われます。

    例:

    const str = 'Hello, World!'
    const substr1 = str.slice(0, 5)
    console.log(substr1) // 出力: 'Hello'
    
    const substr2 = str.slice(-6)
    console.log(substr2) // 出力: 'World!'
    

substring()slice() は似ていますが、以下の違いがあります:

  • substring() は開始位置と終了位置を入れ替えても同じ結果になりますが、slice() は開始位置が終了位置より大きい場合は空の文字列を返します。
  • substring() は負のインデックスを0として扱いますが、slice() は負のインデックスを文字列の末尾からの位置として扱います。

一般的には、slice() を使用することが推奨されます。slice() は配列でも使用されるメソッドであり、一貫性があります。また、負のインデックスを使用できるため、文字列の末尾からの位置を指定しやすくなります。

以上が substr() の代替となるメソッドです。substring() または slice() を使用することで、非推奨となった substr() を置き換えることができます。

蔀

PartialBlockObjectResponse のせいで型推論失敗してるんだけど、この定義何用なんだ

蔀

TypeScriptでは、&(交差型)を使用して複数の型を組み合わせることができます。また、特定のプロパティを除外するには、Omitユーティリティ型を使用することができます。

Omitは、指定された型からプロパティを除外した新しい型を作成するためのユーティリティ型です。構文は以下のようになります:

type OmittedType = Omit<OriginalType, Keys>
  • OriginalType: 元の型
  • Keys: 除外するプロパティのキーを指定するユニオン型

例えば、以下のように使用できます:

type User = {
  id: number
  name: string
  email: string
  age: number
}

type UserWithoutAge = Omit<User, 'age'>
// UserWithoutAge は { id: number; name: string; email: string } 型になる

上記の例では、User型からageプロパティを除外した新しい型UserWithoutAgeを作成しています。

複数の型を&で組み合わせた場合も、同様にOmitを使用して一部のプロパティを除外することができます:

type BaseProps = {
  id: number
  className: string
}

type AdditionalProps = {
  title: string
  description: string
}

type CombinedProps = BaseProps & AdditionalProps

type PropsWithoutClassName = Omit<CombinedProps, 'className'>
// PropsWithoutClassName は { id: number; title: string; description: string } 型になる

上記の例では、BasePropsAdditionalProps&で組み合わせたCombinedProps型から、classNameプロパティを除外した新しい型PropsWithoutClassNameを作成しています。

このように、Omitユーティリティ型を使用することで、&で組み合わせた型定義から特定のプロパティを除外することができます。除外するプロパティを柔軟に指定できるため、型の再利用性が向上します。

Omitは TypeScript 3.5 以降で利用可能です。古いバージョンの TypeScript を使用している場合は、同等の機能を持つカスタムユーティリティ型を定義することもできます。

蔀

はい、TypeScriptでは、Omitユーティリティ型を使用して、交差型から特定の型を除外することができます。ただし、Omitは型のプロパティを除外するためのものなので、少し工夫が必要です。

交差型から特定の型を除外するには、以下のようなユーティリティ型を定義することができます:

type RemoveType<T, U> = {
  [P in Exclude<keyof T, keyof U>]: T[P]
}

このユーティリティ型 RemoveType は、以下のように動作します:

  1. Exclude<keyof T, keyof U> を使用して、型 T から型 U に存在するプロパティのキーを除外します。
  2. P in ... を使用して、除外されたキーを使用して新しい型を作成します。
  3. T[P] を使用して、型 T から除外されたプロパティの型を取得します。

これを使用して、CombinedProps から AdditionalProps を除外することができます:

type BaseProps = {
  id: number
  className: string
}

type AdditionalProps = {
  title: string
  description: string
}

type CombinedProps = BaseProps & AdditionalProps

type PropsWithoutAdditional = RemoveType<CombinedProps, AdditionalProps>
// PropsWithoutAdditional は { id: number; className: string } 型になる

上記の例では、RemoveTypeユーティリティ型を使用して、CombinedPropsからAdditionalPropsを除外しています。結果として、PropsWithoutAdditional型はBaseProps型と同じ型になります。

このアプローチは、Node.jsモジュールの中で定義された交差型に対しても適用できます。モジュールの型定義を直接変更することなく、必要な型を抽出することができます。

ただし、このアプローチには制限があります。除外する型が交差型の一部である必要があります。また、除外する型が複雑な場合や、再帰的な型の場合は、このアプローチでは完全に除外できない可能性があります。

そのような場合は、モジュールの型定義をローカルで拡張したり、型アサーションを使用したりするなど、状況に応じて適切な方法を検討する必要があります。

蔀

↑これは難しいので、Extractか型アサーションで対応する

蔀

このような場合、database.find()が返す型をNotionPage型に適合させるために、型アサーションを使用することができます。

以下のように、asキーワードを使って型アサーションを行います:

const page = database.find((page) => getPageTitle(page as NotionPage) == title)

ここでは、pageNotionPage型にアサーションしています。これにより、getPageTitle関数の引数の型がNotionPage型に適合し、型エラーが解消されます。

ただし、型アサーションは慎重に使用する必要があります。型アサーションを使用する際は、実際のデータが期待する型と一致していることを確認してください。不適切な型アサーションを行うと、ランタイムエラーが発生する可能性があります。

別の方法として、getPageTitle関数の引数の型をQueryDatabaseResponse['results'][number]に変更することも検討できます。

function getPageTitle(page: QueryDatabaseResponse['results'][number]): string {
  // ページタイトルを取得するロジック
}

const page = database.find((page) => getPageTitle(page) == title)

この方法では、getPageTitle関数内でpageNotionPage型の条件を満たしているかどうかを確認し、適切に処理を行う必要があります。

例えば、型ガードを使用してpageNotionPage型の条件を満たしていることを確認できます:

function getPageTitle(page: QueryDatabaseResponse['results'][number]): string {
  if ('properties' in page && typeof page.properties === 'object') {
    // ページタイトルを取得するロジック
  } else {
    throw new Error('Invalid page format')
  }
}

型ガードを使用することで、pageNotionPage型の条件を満たしていない場合に適切なエラーハンドリングを行うことができます。

どの方法を選択するかは、プロジェクトの要件や型の安全性に対する考え方によって異なります。型アサーションは手っ取り早い方法ですが、型の安全性が保証されません。一方、関数の引数の型を変更する方法は、型の安全性を維持しつつ、適切なエラーハンドリングを行うことができます。

プロジェクトの状況に応じて、適切な方法を選択してください。コードの可読性と保守性を考慮しながら、型の安全性とエラーハンドリングのバランスを取ることが重要です。

このスクラップは2024/05/04にクローズされました