denopsで覚えるTypeScript
denops.vimを使うとDenoで動くVimプラグインを作れる
導入方法やdenops固有の使い方は作者による紹介記事に書かれているので省略するとして、TypeScriptわからん人向けの解説があってもいいと思ったのでだらだら書いていく
Deno固有の話は端折っているのでリンク先を辿ってください
JavaScriptに関する知識は端折っている部分があるのでJavaScript Primer辺りを参考にしてほしい
一般的なTypeScriptの話も書いていないのでTypeScript Deep DiveやサバイバルTypeScriptなどと一緒に読むのをおすすめします
TypeScriptは処理系の支援無しに書くのが非常に難しいので(kuuote.iconの感想)、deno lspを使用できる環境をセットアップするのを強くおすすめします
vim-lspではvim-lsp-settingsを導入すると何もしなくても使えるようになる
有効にならないときは親ディレクトリにnode_modulesがないかや、g:lsp_settings_filetype_typescriptの値を確認するといいです
coc.nvimでは:CocInstall denoをした後何かしたらいい(kuuote.iconは覚えてない)
:CocCommand deno.initializeWorkspaceするとdenoプロジェクトとして認識されるようになる
なお、ディレクトリ配下に.vim/coc-settings.jsonが作られるので.gitignore_globalなどで.vimをignoreしておくとよい。
nvim-lspではnvim-lspconfigを導入した上で以下の記述を行う
code:denols.lua
require('lspconfig').denols.setup({
init_options = {
lint = true,
},
})
最低限のdenopsプラグインは以下のコードを所定の位置(プラグインディレクトリに対してdenops/{plugin-name}/main.ts)に置くと成立する
このプラグインは読み込まれた際にHello denops.vimとメッセージを出力するだけのもの
本来は何も書かなくても成立するが、読み込まれたかすら表示されないのでコマンドを実行している
code:main.ts
import { Denops } from "https://deno.land/x/denops_std@v3.1.3/mod.ts";
export async function main(denops: Denops): Promise<void> {
await denops.cmd("echomsg 'Hello denops.vim'");
}
これを書くと、main.tsはDenops型の変数を引数に取るmainという名の非同期関数を提供する役割を果たす
: Denopsや: Promise<void>のようになっている物がTypeScriptの型で、引数や変数の後に付けると、その引数や変数の型、関数宣言とブロックの間に付けると、その関数の返り値の型になる
Denops型の情報はより取り込まれ、main.tsはmain関数を提供する。これはES Modulesという仕様に基づき記述される
MDNのJavaScript モジュール、import、export辺りに詳しく載っている
denops.vimは起動すると'runtimepath'よりdenops/*/main.tsを探してインポート、denopsインスタンスをmain関数に渡して実行する。その際denops.vimから見付けられるようにするためにmain関数にはexportを書く必要がある
非同期関数に関しては、こちらに書かれている
Promiseと呼ばれる物を裏でやりとりしているが、これを直接操作することはあまりないと思うので一旦は値を返す時は型をPromise<>で包まないといけないことだけ意識するとよい
関数宣言の際にasyncキーワードを付けると非同期関数になり、Promiseを返すようになる、中ではawaitキーワードで別のPromiseを待てるようになる
上の例ではdenopsに送ったコマンドの終了を待っている
awaitを忘れても何の警告も出ないまま浮いたPromiseが発生するので注意
denopsはこの非同期処理を用いてVimとやりとりするため重要
denopsからVimへのやりとりはdenopsインスタンスを使い自由に行える仕組みになっている。逆にVimからdenopsへのやりとりはdenops.dispatcher以下にハンドラーを追加することにより行う。
作者によるチュートリアルに従うとこの形になる
code:main.ts
import { Denops } from "https://deno.land/x/denops_std@v3.1.3/mod.ts";
import { ensureString } from "https://deno.land/x/unknownutil@v1.1.4/mod.ts";
export async function main(denops: Denops): Promise<void> {
denops.dispatcher = {
async echo(text: unknown): Promise<unknown> {
ensureString(text);
return await Promise.resolve(text);
},
};
};
denops.dispatcherは以下の型で表現される
code:denops.ts
export interface Dispatcher {
key: string: (...args: unknown[]) => Promise<unknown>;
}
これは、任意の名前の「unknown型の引数を任意個受け取りPromise<unknown>型の関数」を持つオブジェクトを示す
言い換えるとdenops.dispatcherに渡すオブジェクトの要素となる関数は、必ずunknown型の引数を受け取りunknown(あるいはvoidも許容される)型の値を返す非同期関数でなければならない
unknownは、その値が検査されていないことを示す特別な型で、TypeScriptの仕組み上型だけでマッチしない値を弾けないため一時的に使用するためのもの ( unknown以外の型になるまでコンパイラに型固有の操作が弾かれる)
同様の型としてanyが存在するが、unknownがほとんど何もできないのに対しanyは何でもできるため基本的に使用は避けるべきである
echo関数の中で(上記の関数ではそのまま返しているだけだが)引数を扱うには正しい物かを識別する(そしてTypeScriptに教える)必要がある。これには型ガードという仕組みを用いるが、標準の方法はJavaScriptの構文を踏襲しているためいささか不便である
例えばensureStringを本来の形に直すとこうなる
code:ensure.ts
if (typeof x !== "string") {
throw new EnsureError("The value must be string");
}
TypeScriptではこの型ガードを自分で定義できる、上で使っているensureStringはそうやって定義されたものでdenops.vimの作者、ありすえ氏の作ったunknownutilというライブラリが提供する関数である
unknownutilは型ガードを各種提供しているためunknownを多用するdenopsプラグインでは便利
このライブラリは短いので型ガードの定義方法を知りたければ読んでみるといいと思うkuuote.icontakker.icon
(脱線)このライブラリをweb application開発でつかおうとすると型エラーが出るので注意
ensureStringは渡された引数がstring型でなければ例外を投げ、TypeScriptのチェッカーには渡された物を以降stringとして扱っていいことを教える役割を果たす
これによりechoは与えた引数が文字列であれば返し、そうでなければエラーが発生する関数になる
作った関数は、チュートリアルに書いてある通り、Vimから:echo denops#request('helloworld', 'echo', ["Hello Denops!"])のように実行できる