Stable Diffusion web UIで「No Python at」で起動できなくなった場合の対処法

友人から、「PC買い換えてStable Diffusion web UIを移動したら起動できなくなった」という話を聞いた。

状況聞いて解決できたが、エンジニアじゃない人にはちょっと面倒かなあと思いメモ。

問題

Stable Diffusion web UIをフォルダごと、古いPC(Windows 10)から新しいPC(Windows 11)にコピー。この時、保存先は同じパスにした模様。

新しいPCでGit for WindowsおよびPython3.10をインストールし、Stable Diffusion web UIの webui-user.bat を実行すると、 No Python at 'C:\Python310\python.exe' で起動に失敗するとのこと。

Pythonは、古いPCでは C:\Python310 にインストールした模様。新しいPCではMicrosoft Storeからインストールしたため、 どこにインストールされたかはわかっていなかった。

調査

webui-user.bat の内容を教えてもらうと、 set PYTHON= があったので、そこに where python した結果( C:\Users\<ユーザー名>\AppData\Local\Microsoft\WindowsApps\python.exe )を設定してもらったが変わらず。

上記のパスの python.exe は0KBのため、シンボリックリンクっぽい。実ファイルパスを入れてみるかとも思ったが、それよりも set VENV_DIR=webui-user.bat にあるのが気になる。

エラーメッセージと venvGitHubを検索すると、以下のissueが見つかった。

github.com

こちらのコメントにあるように、 venv フォルダを削除すればよい。

対応

webui-user.batVENV_DIR を設定していればそのディレクトリ、未設定であれば webui-user.bat と同じフォルダ内の venv フォルダを削除(Windowsのゴミ箱に入れればいい)し、 webui-user.bat を実行することで解消した。

このフォルダには、PythonがStable Diffusion web UIを実行するために必要なモジュール等の保存をしているが、そこに webui-user.bat を最初に実行したときのPythonのパスも保存されており、それが原因で動かなくなっていた模様。

削除後の実行では、モジュールのダウンロード等が再度実行されるため多少時間はかかるが、 venv フォルダが一番手っ取り早い。

同じPC上でも、Pythonを更新してパスが変わった場合には、同様の問題が発生しそう。また、フォルダを移動したら動かなくなった、という話もちらほら見かけたが、それも同様の原因だと思う。

なお、設定ファイルなどは venv に保存されていないため、安心して削除していい。

ユーザー設定は、Settingsタブでの設定が config.json 、txt2imgやimg2imgタブの設定が ui-config.json、保存したStyleは styles.csv に保存されている模様。

git pull で更新できない問題

ついでに、「git pull で更新しようとしたらできない」ということで相談を受けた。

git status してもらうと、一部の py ファイル等に変更があるが、心当たりはないとのこと。

適当なファイルをメモ帳で開いてもらうと、改行コードがCRLFになっていた。Git Bashgit config --get core.autocrlf を実行してもらうと true となっていたため、それが原因でpullした際に改行コードが変換され、それが変更差分として扱われたものと思われる。

git config --global core.autocrlf false を実行してもらい、リポジトリの変更は git reset --hard で切り戻した。

config.json など、ユーザー設定ファイルや、 models , outputs といったフォルダ配下の変更は差し戻されないか聞かれたが、いずれも .gitignore に記載されているため、対象外となっている。

注意点として、 webui-user.bat および webui-user.sh.gitignore に記載されているが、すでにGit管理下に含まれているため、おそらくユーザーが変更する可能性のあるファイルのうち、これらのファイルに加えた変更のみ git reset で差し戻される可能性がある。

友人は COMMANDLINE_ARGS を変更していたため、そこだけバックアップしてもらい、 git reset --hard および git pull を実行してもらうと、最新の状態に更新できた。

振り返り

インストールにGitを使ってPythonで動かすとか、なかなか非エンジニアにはインストールが面倒そうだなぁと思った。

最近はDockerでも動かせるようだが、Docker使うよりはGit+Pythonのほうが楽か。

しかし、ChatGPTといいStable Diffusionといい、すごい時代になったなぁ。

ところで、Stable Diffusion web UIのメインのブランチ名、 master なのね。すっかり main に慣れたので、逆に珍しいと感じた。

LangChainのドキュメントのURLが変わっていた

前回書いたLangChain.jsのサンプルプロジェクトの設定と実行を、別PCで試してみたらどうもうまく動かない。

なんでだろうと思って調べると、本家のPython版LangChain、およびLangChain.jsの公式ドキュメントのURLが変わっていたのでメモ。

変更前後のURL

Python版LangChain、JavaScript版LangChain、両方ともドキュメントのドメインが変更されていた。

langchain.com独自ドメインとして、プログラミング言語名をサブドメインにする形になった模様。

JavaScript版のドメインは、2023/3/28の午後の時点で切り替わっていることを確認。

Python版にはリダイレクトが設定されていたが、JavaScript版はリダイレクトも設定されておらず、以下のissueの添付画像のように、崩れた画面が表示されていた。

github.com

この記事を書いている2023/3/31時点では、JavaScript版の旧ページは404になる。また、どうやらページ階層が変わったらしく、単純にドメインを更新後のものに変えただけでは開けないページが多い。

サンプルプロジェクトの修正

前回のサンプルプロジェクト設定で、Python版ドキュメントをダウンロードしているが、ドメインが変更されているため、対応する必要がある。

変更箇所は2つ。

  1. download.shwget 対象URLを、 https://langchain.readthedocs.io/en/latest/ から https://python.langchain.com/en/latest/ に変更
  2. ingest.tsconst directoryPath に設定する文字列を、 langchain.readthedocs.io から python.langchain.com に変更

また、Gitで管理する場合、 .gitignorelangchain.readthedocs.io/ が記載されているので、 python.langchain.com/ も追加しておくといい。

これらの変更で、ドメイン変更前と同様に動くようになった。

振り返り

仕事でまとめたドキュメントのリンクが全滅したので、不貞寝します。

WindowsでJavaScript版LangChainを動かす

「ChatGPT使って何かやりたい」的なふわっとした要望のもと、OpenAI APIの検証をすることになった。

APIを素で叩くのはちょっと辛いなあと思っていたので調べると、LangChainを使うのがよさそうに思えたが、チーム内でPython使えるのが自分しかいない。

他のプログラミング言語で扱えないかと思ったところ、JavaScript版であるLangChain.jsがnpmパッケージとして提供されており、TypeScriptもサポートされているため、そちらを使うことにした。

Windowsで環境構築する場合、いくつか注意点があったのでメモ。

環境

Windows 10 Pro 21H2, Node.js 16.15.1, Yarn 1.22.19, Python 3.10.10, Git 2.39.2 にて、2023/3/13に確認。

OpenAIのAPIキーは取得済み。

サンプルプロジェクトのダウンロード

ゼロベースで環境構築するのは、画面作成などが面倒。

LangChainブログのTypeScriptサポート記事で、Next.js製のサンプルプロジェクトが紹介されていたので、そちらを利用する。

github.com

yarn create next-app langchain-example \
  -e https://github.com/sullivan-sean/chat-langchainjs

サンプルプロジェクトの設定

以降、READMEに沿って設定していく。コマンド実行にはGit Bashを使用。

READMEではパッケージマネージャーとしてYarnが使われているためそれに倣うが、npmなど他のパッケージマネージャーでも問題ないと思われる。

.env ファイルの作成

.env.example ファイルが用意されているので、これを .env としてコピー。

cp .env.example .env

2023/3/13 時点で .env ファイルの内容は OPENAI_API_KEY="" のみのため、ここにOpenAIのAPIキーを設定しておく。

npmパッケージのインストール

yarn install を実行すると、以下のエラーが発生。

gyp ERR! find Python
gyp ERR! find Python Python is not set from command line or npm configuration
gyp ERR! find Python Python is not set from environment variable PYTHON
gyp ERR! find Python checking if "python3" can be used
gyp ERR! find Python - "python3" is not in PATH or produced an error
gyp ERR! find Python checking if "python" can be used
gyp ERR! find Python - "python" is not in PATH or produced an error

LangChain.jsのREADMEにあるように、Python版のLangChainを使用しているため、Pythonのインストールが必要。仕事用PC、一度初期化してからPython入れていなかった。

任意の方法でPythonをインストールし、PATHを通すなどで実行可能にしておく。現時点のWindows 10では、Pythonがインストールされていない状態でコマンドプロンプトから python を実行すると、Microsoft StoreのPython 3.10のページが開くので、今回はそこからPython 3.1010をインストールした。

再度 yarn install すると、別のエラー。

error <PROJECT_DIR>\node_modules\hnswlib-node: Command failed.
Exit code: 1
Command: node-gyp rebuild
Arguments:
Directory: <PROJECT_DIR>\node_modules\hnswlib-node
Output:
<PROJECT_DIR>\node_modules\hnswlib-node>if not defined npm_config_node_gyp (node "C:\Program Files\nodejs\node_modules\npm\bin\node-gyp-bin\\..\..\node_modules
\node-gyp\bin\node-gyp.js" rebuild )  else (node "" rebuild )
gyp info it worked if it ends with ok
gyp info using node-gyp@9.0.0
gyp info using node@16.15.1 | win32 | x64
gyp info find Python using Python version 3.10.10 found at "<AppData>\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\python.exe"
gyp ERR! find VS
gyp ERR! find VS msvs_version not set from command line or npm config
gyp ERR! find VS VCINSTALLDIR not set, not running in VS Command Prompt
gyp ERR! find VS could not use PowerShell to find Visual Studio 2017 or newer, try re-running with '--loglevel silly' for more details
gyp ERR! find VS looking for Visual Studio 2015
gyp ERR! find VS - not found
gyp ERR! find VS not looking for VS2013 as it is only supported up to Node.js 8
gyp ERR! find VS
gyp ERR! find VS **************************************************************
gyp ERR! find VS You need to install the latest version of Visual Studio
gyp ERR! find VS including the "Desktop development with C++" workload.
gyp ERR! find VS For more information consult the documentation at:
gyp ERR! find VS https://github.com/nodejs/node-gyp#on-windows
gyp ERR! find VS **************************************************************
gyp ERR! find VS
gyp ERR! configure error

URL付きで説明してくれているが、node-gypによるビルドにVisual C++ build toolsが必要。これまた初期化してからインストールしていなかった。

今回は、Chocolateyにパッケージがあったので、管理者権限で実行したコマンドプロンプトから、 cinst -y visualcpp-build-tools でインストール。

また、 npm config set msvs_version 2017 しておく必要があるとのこと。プロジェクト単位で設定したいので、 echo 'msvs_version=2017' > .npmrc.npmrc に記述する。

この状態で yarn install すると成功。

関連パッケージの更新

2023/3/13時点で、プロジェクトに設定されたLangChainおよびOpenAIパッケージのバージョンが古くなっている。

設定されているのはLangChainが0.0.15、OpenAIが3.1.0だが、最新はLangChainが0.0.29、OpenAIが3.2.1のため、 yarn add langchain@0.0.29 openai@3.2.1 でそれぞれ更新する。

すると、 pages/api/util.tsコンパイルエラーが発生。該当部分は、 const docChain の宣言時の callbackManager の部分。

callbackManager: {
  handleNewToken: onTokenStream,
}

import { CallbackManager } from 'langchain/callbacks' を追加し、 callbackManager を以下のように変更することで解消した。

callbackManager: onTokenStream
  ? CallbackManager.fromHandlers({
      handleLLMNewToken: async (token) => onTokenStream(token),
    })
  : undefined,

dataディレクトリ配下の情報の更新

この状態でも yarn dev すれば動くが、dataディレクトリに保存されているLangChainのドキュメント情報が古いため、更新を行う。

Langchain docsのダウンロード

プロジェクト直下の download.sh を実行すればいいが、 wget を使用しているため、ChocolateyなりScoopなり、バイナリをダウンロードなり、任意の方法でインストールしておく。

wget が実行可能な状態であれば、 download.sh を実行すると、Python版LangChainのドキュメントlangchain.readthedocs.io ディレクトリにダウンロードされる。

解析処理の実行

yarn ingest を実行すると、 langchain.readthedocs.io ディレクトリ配下のHTMLを解析し、結果が data ディレクトリ配下に保存されるはずだが、実行するとエラー。

Error: Wrong space name, expected "l2" or "ip".
    at Function.getHierarchicalNSW (<PROJECT_DIR>\node_modules\langchain\src\vectorstores\hnswlib.ts:53:12)
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at HNSWLib.initIndex (<PROJECT_DIR>\node_modules\langchain\src\vectorstores\hnswlib.ts:61:20)
    at HNSWLib.addVectors (<PROJECT_DIR>\node_modules\langchain\src\vectorstores\hnswlib.ts:85:5)
    at Function.fromDocuments (<PROJECT_DIR>\node_modules\langchain\src\vectorstores\hnswlib.ts:195:5)
    at run (<PROJECT_DIR>\ingest.ts:77:23)
    at <anonymous> (<PROJECT_DIR>\ingest.ts:82:3)
error Command failed with exit code 1.

hnswlibがサポートするDistanceに cosine が追加されたが、hnswlib-nodeが古いため対応していない模様。

github.com

該当のソースコードnode_modules/langchain/dist/vectorstores/hnswlib.js 。0.0.15では ip が指定されていたが、0.0.29では cosine に変更されていた。

2023/3/13時点で、最新のhnswlib-nodeは1.4.2のため、 yarn add hnswlib-node@1.4.2 で更新すると、 yarn ingest が成功するようになった。

注意点として、解析中に fetch が実行されるため、Node.js v18未満の場合、 NODE_OPTIONS='--experimental-fetch' yarn ingest のように、 --experimental-fetch オプションをつけて実行する必要がある。

サンプルプロジェクトの実行

yarn dev し、 localhost:3000 を開く。

メッセージとして Hi there! How can I help? が表示されていれば成功。画面下の入力欄から質問をすることが可能。

自己紹介をさせてみた

pages/api/util.ts にプロンプトが記載されている。回答は Answer in Markdown: のため、Markdown形式となる。

なお、モデルとしては、 OpenAIインスタンス生成時に modelName を指定していないため、デフォルトの text-davinci-003 が使用される。

振り返り

Python版LangChainのビルドにもろもろ準備が必要だが、それ以外はスムーズに環境構築できた。

wget でダウンロードするURLを変更し、 ingest.ts での解析対象のディレクトリ名を変えて yarn ingest を実行、 pages/api/util.ts のプロンプトを変更してやるだけで、それっぽいふるまいをするようになる。

なんというか、すごい時代になったなぁ...

余談

LangChainの質問を投げていると、URL付きで自信満々っぽく回答してくれるが、回答内容が間違っていたり、URLも404だったりして笑う。

一度、存在しないメソッドの説明をしてくれたこともあった。

プロンプトで以下のように指定されているが、効いてないんだろうか。

You should only use hyperlinks that are explicitly listed as a source in the context. Do NOT make up a hyperlink that is not listed.
If you don't know the answer, just say "Hmm, I'm not sure." Don't try to make up an answer.

Notioin AIにサンプルコードを提示されて驚いたこと

期間限定で無料開放されているNotion AIを使っていて、驚いたことがあったので備忘録。

出来事

Node.js のアプリケーション上で、 Chart.js を用いたグラフ表示を行っている。

いくつか要望があったが、そのうち「グラフのX軸のラベルの角度を調整したい」という お偉いさんの思い付きのどうでもいい ものを調査し、結果をNotionに書き込もうとしたときの出来事。

Notionに「 X軸のテキストの角度を調整する」と書いて、それを見出しに変更しようとしたが、この時誤ってAIに続きを書くよう依頼してしまった。すると、以下のような回答。

「X軸のテキストの角度を調整する」へのNotion AIの回答

さて、この回答自体は問題ない。 options.scales.x.ticksminRotationmaxRotation を指定することで角度を調整できることは調査済みだった。

Common tick options to all cartesian axes | Chart.js

この時驚いたのは、書いていたページ内に、「Chart.jsを使用している」ことは明記していなかったこと。

他のページを参照しているのか、ページ内の情報から推論したのか、どうやって判断したのか気になったので、追加で質問してみる。

追加質問

ありがとうございます。質問なのですが、提示されたサンプルコードはChart.jsのコードに見えます。また、実際にグラフを描画するのに使用しているライブラリはChart.jsになります。ですが、このページ内にて、Chart.jsを使用するとは明示しておりません。あなたはどのようにして、グラフ描画にChart.jsを使用していると推論したのでしょうか。推論の過程および結果を教えてください。

この質問に対しては、こんなことを回答してくる。自己評価低いなNotion AI。

申し訳ありません、前回の回答に誤りがありました。おっしゃる通り、このページにはChart.jsを利用している旨の明示がありませんでした。私の回答は、不正確であることを認めます。

その都度、「あなたは間違ってないです、Chart.jsを使ってます、どうやってそう判断したの?」みたいなことを繰り返し質問すると、3回目くらいで以下の回答。

Chart.jsを使用していると推論した理由

Chart.jsを使用していることは、 scales オプションを指定していることから推測されます。Chart.js公式ドキュメントにおいて、 scales オプションはChart.jsで使用されるオプションの一つであり、その記述があるためです。また、Chart.jsは比較的よく使われているグラフ作成ツールであることも、推論の根拠となります。

確かに、この質問をしたページには、それまでの調査結果のコードを張り付けていたので、以下のようなJSONを複数記述していた。

options: {
  scales: {
    ...
  }
}

感想

options.scales といったコード片と、グラフという情報からChart.jsと推論したのはすごい、感心してしまった。

ページ内の情報を元に回答してくれるのも大きい。テキストを書きなぐっておいて、それに関した情報収集やブレインストーミング、文章校正が可能なのは、ChatGPTのような対話型にはない強みだと思う。

また、Notion AIに限らずだが、プログラミングをする場合の技術選定にAIチャットは使えそう。AIの学習データが収集された時点と、公開までに時差があるため、最新技術などを使うのには向かないが、例えばChatGPTの2021年9月までの情報でも、開発に用いるライブラリやフレームワークアーキテクチャの提案をさせるのには十分だろう。逆に、AIチャットが知らない・答えられない程度のライブラリ等は使いづらくなるなとも感じた。

会社の予算的に、無料開放終わったらNotion AI契約しないと思うので、使えるうちに遊んでおこうっと。

余談

NotionAI自体ってどう実装されているか気になったので聞いてみたところ、以下のような回答。

  • Notionの独自実装、GPTとかは使っていない
  • 開発には膨大な時間と技術が必要だったと思われるが、詳細な情報は公式からの発表がないため、自身も把握していない
  • クラウドサービスなど外部のサービスを使っているかもわからない

GPTを使っていないと、外部のサービスを使っているかわからないが矛盾しているような。

自身のオリジンを知らないAI、漫画や映画のキャラクターっぽい。

参考

Notion AIについてのインタビュー。

www.businessinsider.jp

検索は苦手と言っているが、 Chart.js の例を見ると十分得意だと思う。ワークスペース全体の情報をAIが参照できるようにする予定もある模様。

現状では実行したページ以外の情報も参照していない。そのため、Notion内での「探索」やページをまたいでの要約生成などもできない。

ただしこの点について、Notionは今後「ワークスペース全体」を参照できるように拡張する計画がある。

ワークスペース全体を参照し、このクオリティで回答してもらえるのであれば、ナレッジベースとして圧倒的な強みになりそう。

Notionに限らず、たとえばAlgoliaあたりがAIチャットボットを提供したりしないかな。

2023/3/13 追記

LangChainJSのチャットに使われているMendable.aiが、ドキュメントを基にしたAIチャットボットっぽい。

www.mendable.ai

オープンソースプロジェクトであれば、現状は無料で使えるみたい。

TypeScriptのMapped Typesでのプロパティの型指定方法いろいろ

前回、ちらっとMapped Typesのプロパティの型を extends で切り替える方法を書いたが、他にもいろいろな指定方法があるのでメモ。

環境

TypeScript v4.7.4 で確認。

実装例

以下のtypeを用意。

type ExampleType = {
  one: string
  two: number
  three: boolean
}

直接指定

最もシンプル。

type ExampleMappedType = {
  [P in keyof ExampleType]: string
}

const example: ExampleMappedType = {
  one: 'one',
  two: 'two',
  three: 'three',
}

Mapped Typesの記法は { [P in K]: T } だが、直接指定の場合 Record<K, T>に置き換えられる。

Conditional Types

前回書いたが、Conditional Typesが使える。

type ExampleMappedType = {
  [P in keyof ExampleType]: P extends 'one' ? string : number
}

const example: ExampleMappedType = {
  one: 'one',
  two: 2,
  three: 3,
}

P を用いた型指定

{ [P in K]: T }P を用いた T の型指定が可能。

プロパティ参照

ブラケット表記法によるプロパティ参照ができる。

type ExampleMappedType = {
  [P in keyof ExampleType]: ExampleType[P]
}

const example: ExampleMappedType = {
  one: 'one',
  two: 2,
  three: false,
}

参照する型定義は、 P に含まれるプロパティが存在すればいいため、 [P in K]K の型である必要はない。

type ReferenceType = Record<keyof ExampleType | 'four', string>

type ExampleMappedType = {
  [P in keyof ExampleType]: ReferenceType[P]
}

const example: ExampleMappedType = {
  one: 'one',
  two: 'two',
  three: 'three',
}
関数の引数や戻り値

関数にすることで、 P を引数や戻り値の型に使える。

as によるKey Remappingでプロパティ名の変換と合わせて使うと、プロパティ名に対応した関数を宣言できる。

type ExampleMappedType = {
  [P in keyof ExampleType as `format${Capitalize<P>}`]: (value: ExampleType[P]) => string
}

const example: ExampleMappedType = {
  formatOne: value => `one: ${value}`,
  formatTwo: value => `two: ${value}`,
  formatThree: value => `three: ${value}`,
}

Key Remapping中にConditional Typesの判定を挟むことも可能。

type ExampleMappedType = {
  [P in keyof ExampleType as P extends 'three'
    ? `is${Capitalize<P>}`
    : `get${Capitalize<P>}`]: () => ExampleType[P]
}

/* これでも同じ
type ExampleMappedType = {
  [P in keyof ExampleType as `${P extends 'three'
    ? 'is'
    : 'get'}${Capitalize<P>}`]: () => ExampleType[P]
}
*/

const example: ExampleMappedType = {
  getOne: () => 'one',
  getTwo: () => 2,
  isThree: () => true,
}

参考

以下のページが詳しいが、使われている用語が公式ハンドブックにまとめられていない模様。

zenn.dev

www.typescriptlang.org

型引数 P, K, T の説明に使われている用語は、Mapped types追加のPRから取ったものか?

github.com

homomorphic mapped type は、TypeScriptのissueやFAQに出てくる。

github.com

Stack Overflowにも質問があった。

stackoverflow.com

振り返り

Mapped Typesの型指定時にConditional Typesとブラケット表記を使ったところ、これまたコードレビューで「なにこれ?」と聞かれたので書いてみた。

Conditional Typesは、型を指定できるところでは当然のように書けるので、いつの間にやら使っていたが、Mapped Typesと組み合わせて使う例はWeb検索してもあまり出てこない気がする。

TypeScriptで条件によってプロパティの型を切り替えたり、プロパティを削除する

TypeScriptで、typeやinterface、classの総称型によって、プロパティの型を切り替えたり、削除したりすることができるのでメモ。

環境

TypeScript v4.7.4 で確認。

方法

Conditional Types という機能を用いる。日本語だと「条件付きタイプ」、「条件型」あたりか。

www.typescriptlang.org

記述は以下、条件演算子(三項演算子)に似ている。

SomeType extends OtherType ? TrueType : FalseType

上記の例だと、 SomeTypeOtherType として扱える場合 TrueType 、扱えない場合 FalseType となる。

実装例

こんなUnionを用意。

type ExampleUnion = 'one' | 'two' | 'three'

プロパティの型の切り替え

type 等で総称型を指定できるようにして、型を切り替えられる。

type ExampleType<T extends ExampleUnion> = {
  value: T extends 'one' ? string : number
} // Recordの第2引数など、型を記述できるところであればどこでも使える
  & Record<"records", Array<T extends 'two' ? number : string>>
  // ここは丸かっこで囲わないと、最初の設定が消える
  & (T extends 'three' ? { three: string } : { other: number })

const one: ExampleType<'one'> = {
  value: 'one',
  records: ['one'],
  other: 1,
}

const two: ExampleType<'two'> = {
  value: 2,
  records: [2],
  other: 2,
}

const three: ExampleType<'three'> = {
  value: 3,
  records: ['three'],
  three: 'three',
} 

また、Mapped Typesを宣言するときも、 extends で一部のプロパティの型を変更できる。

type ExampleType = {
  [P in ExampleUnion]: P extends 'one' ? string : number
}

const example: ExampleType = {
  one: 'one',
  two: 2,
  three: 3,
}

プロパティの削除

Omit の第2引数で判定し、対象となるプロパティを指定する。

また、型の切り替えの例のように、交差型を使ってもいい。

type ExampleType<T extends ExampleUnion> = Omit<
  {
    value: string
    values: string[]
  },
  T extends 'one' ? 'values' : T extends 'two' ? 'value' : never
>

const one: ExampleType<'one'> = {
  value: 'one',
}

const two: ExampleType<'two'> = {
  values: ['two'],
}

const three: ExampleType<'three'> = {
  value: 'three',
  values: ['three'],
}

参考

以下のページが、 Inferring Within Conditional TypesDistributive Conditional Types についての記載もあり詳しい。

zenn.dev

振り返り

前回と同じく、コードレビューしてもらったときにレビュアーが知らなかったので書いてみた。

ライブラリやフレームワークのソースを見ると、よく使われている。何かのタイミングで、「なんだこれ?」と思って調べたのがこの機能を知ったきっかけ。

Template Literal TypesといいConditional Typesといい、型を柔軟に扱える機能だが、濫用すると可読性が下がるので注意が必要。

総称型を指定した状態で型をexportするなど、実装時に考慮をしておいたほうがいいだろう。

TypeScriptの型から、プロパティ名を部分一致で抽出したり削除する

TypeScriptで、既存の型から一部のプロパティを、プロパティ名の部分一致で抽出・削除する方法をメモ。

環境

TypeScript v4.7.4 で確認。

プロパティ名の部分一致での抽出・削除方法

テンプレートリテラル内にUnionを用いると、すべての組み合わせの文字列リテラルのUnionとなる。

文字列から型定義が行えるこの機能は、Template Literal Typesというらしい。

TypeScript: Documentation - Template Literal Types

Extract の第2引数に prefix${string} のように指定することで、プロパティ名を部分一致で抽出できる。

Extract の結果を、 Pick に渡してやれば抽出、 Omit に渡してやれば削除できる。

// val1 | val2 | value1 | value2 のUnionになる
type PropertyNames = `${`val${'' | 'ue'}`}${1 | 2}`

// Unionの各値をプロパティ名とした型を用意
type Properties = Record<PropertyNames, string>

// value1 | value2 のUnion
type TargetPropertyNames = Extract<keyof Properties, `value${string}`>

// Record<'value1' | 'value2', string> と同様
type PickProperties = Pick<Properties, TargetPropertyNames>

// Record<'val1' | 'val2', string> と同様
type OmitProperties = Omit<Properties, TargetPropertyNames>

実装例

以下のような type を用意。

type PropertySuffix = 1 | 10 | 100 | 'Value' | 'True' | 'False' | true | false

// { [Key in `string${PropertySuffix}`]: string } & ... と同様
type Properties = Record<`string${PropertySuffix}`, string> &
  Record<`number${PropertySuffix}`, number> &
  Record<`boolean${PropertySuffix}`, boolean>

type PropertyKeys = keyof Properties

任意の文字列での一致

${string}ワイルドカードのように使用できる。

  • 前方一致: prefix${string}
  • 部分一致: ${string}infix${string}
    • infixで指定した文字列が先頭や末尾に存在する場合も対象となる
  • 後方一致: ${string}suffix
// stringで開始するプロパティを抽出
type Type1 = Pick<Properties, Extract<PropertyKeys, `string${string}`>>

// 末尾のeも対象のため、 { string1, string10, string100 } を除いて削除
type Type2 = Omit<Properties, Extract<PropertyKeys, `${string}e${string}`>>

// `False` および `false` を抽出
type Type3 = Pick<Properties, Extract<PropertyKeys, `${string}se`>>

// 3つ以上 ${string} を書くこともできる

// nの後ろに10があるプロパティを抽出
type Type4 = Pick<Properties, Extract<PropertyKeys, `${string}n${string}10${string}`>>

任意の数値での一致

${number} で、数値のみを指定できる。

// { number1, number10, number100 } を抽出
type Type5 = Pick<Properties, Extract<PropertyKeys, `number${number}`>>

true/falseでの一致

これは使う機会がないと思うが、いちおうできるということで。

${boolean} で、 true または false を指定できる。大文字小文字の区別あり。

// { booleantrue, booleanfalse } を抽出
// { booleanTrue, booleanFalse } は対象外
type Type6 = Pick<Properties, Extract<PropertyKeys, `boolean${boolean}`>>

指定文字列や数値での一致

任意のUnionを指定することで、より細かい判定が可能。

// string または number で始まり、数字 or 'Value' or true で終わるものを抽出
type Type7 = Pick<
  Properties,
  Extract<PropertyKeys, `${'string' | 'number'}${number | 'Value' | true}`>
>

振り返り

自分の書いたソースをコードレビューしてもらったときに、「これ何やってんの?」と聞かれたので書いてみた。

ただ、けっこう便利なので使っていたが、いつどこで覚えた方法なのか思い出せない。Stack Overflowで引っかけたのか、どこかのブログでも見たのか。

この記事を書くに際し、公式ドキュメントも見てみたが、${string} が検索しづらく、Template Literal Typesのページ以外見つけられなかった。

何かしら名前がついてそうだが、なんていうんだろう。