2022/05/22

[typescript] インデックスシグネチャ?

覚えたい言語がいくつかある。 TypeScript, Kotlin, Go, Rust。
Rust が気持ちとしては強いのだけど、お仕事で今のところ使っていないので優先度が下がっている。
TypeScript と Go が一番多いのだが、どうやってもできないときに調べるくらいで、あとは適当に書いたら適当に動く(他のところをまねするのも含む)ので、なんとかなっている(と思いたい)。

それでもたまには本を読んで勉強する。たまにはじゃダメだろうと思うが許してほしい。
また OJT だけだと出てこないやり方に気付かないこともあるので、そこそこ体系的な勉強もいると思うのだ。

 

今日、 TypeScript の本を読んで出てきたのが「インデックスシグネチャ」だ。


TypeScript の本に出てきたのだが、インデックスシグネチャ自体は JavaScript から存在する書き方らしい。
MDN で検索したけど出てこないので、よくわからん。

オブジェクトのメンバーを参照するとき、普段は xxObj.yyItem のようにドットで指定するが、これを xxObje['zz'] のように参照するやり方のようだ。ドットで指定するやり方だと実装時にわかっていないと書けないが、添字で指定できるなら動的に参照できるというのがメリットだと思う。

Index signature(インデックス型) - TypeScript Deep Dive 日本語版
https://typescript-jp.gitbook.io/deep-dive/type-system/index-signatures

JavaScript の方を良くしらんのだが、TypeScript の場合はインデックスとして使えるのが string か number に限定されるとのこと。

 

なんとなくわかったつもりになったが、これってキーとなる型が number か string の連想配列と考えてもよいのではないだろうか。
連想配列だったら Map がある。

Map - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map

同じことができそうなのだが、違いは何だろうか? JavaScript 自体には連想配列はこれだ、みたいな定義が無いとかかもしれんが、 Object と Map がそういう立ち位置のようだ。

いろいろ覚えたくない人としては Map だけ覚えておけばいい、と誰かに言ってもらえると助かるのだがね。

2022/05/15

[typescript] ちょっと触った感想

週末に typescript を少し触っていたので感想を残しておこう。

 

トランスパイル後のjsファイルもcommitしたくなる

ts ファイルがあってもトランスパイルしないと node.js で動かせない。
トランスパイル使用としたら tsc をインストールしないといかん。
大したことじゃないのだけど、全然大した作業じゃないのだけど、クラウドに VM 立てて、 node.js と npm が使えるようになって、 git clone してさあ npm start、ってやったら js がないってなったときにちょっとガックリするのだよね。

ts-node がよく紹介されるので使っていたけど、リポジトリ見たら結構大きい・・・。
それなら Makefile 作って、make install、make、npm start、の 3ステップくらいにしたらよいんだろうか。
でも make も何かインストールしないと使えないので、それはそれでって感じだ。

今回は gRPC のためにやっていたので proto ファイルもある。
proto ファイルからツールを使って d.ts ファイルと js ファイルを作ってもらい、それを使った ts ファイルを作り、トランスパイルして js ファイルを作ってもらう。

最後には proto も ts もいらなくなるのだから、なんとなく悲哀を感じてしまった。
node.js みたいに node.ts とかあるとよいのだけどね。

 

トランスパイルしたファイルのディレクトリ

tsconfig に "outDir" があったので "./out" にしてみた。
proto ファイルから d.ts ファイルと js ファイルを作るので、それは "./proto" に置いてみた。
そうすると tsc は成功するのだが node.js で実行しようとするとエラーになる。
import する場所が "./proto" なのだけど js ファイルがあるのは "./out" なので、実際には "../proto" にあることになるからだ(あとから js ファイルを ./out/proto に置けば良いことに気付いたがね)。

rootDirs に "./proto" を追加して import は相対パスで指定すればよいと思ったのだけどダメだった。
「This does not affect how TypeScript emits JavaScript」とあるからトランスパイル後のパスまでは考慮しないってことだと思うけど、tsc で効いてない理由が分からん。

私は挫折して d.ts ファイルをカレントディレクトリに持ってきて "./" で参照するようにし、js ファイルは "./out" に置くようにした。

TypeScript の paths はパスを解決してくれないので注意すべし! – 自主的20%るぅる
https://www.agent-grow.com/self20percent/2019/03/11/typescript-paths-work-careful/

これは paths なのでちょっと違うけど、import が解決できないという意味では同じだ。 ts-patch , typescript-transform-paths を使ってみたが、なんかもう一手間いる感じがした。

[typescript] gRPCはどうやるのがよいんだかわからん

前回、import したファイルを export することについて調べていた。

https://blog.hirokuma.work/2022/05/typescript-import.html


が、元々は gRPC というか proto ファイルだけある状態で呼び出すコードを typescript で書きたいけどどうすりゃいいんだ、というのが目的だった。

proto-loader-gen-types を使って自動生成された tsファイルが大量にあったのでまとめようとしたのだが、よく見るとまとめているようなファイルも生成されていることに気付いた(つまり export で調べたことは関係なくなった)。言い訳だが、ファイルが大量にあって気付かなかったのだ。

別のツールを使っているサイトを探してみた。

OK Google, Protocol Buffers から生成したコードを使って Node.js で gRPC 通信して | メルカリエンジニアリング
https://engineering.mercari.com/blog/entry/20201216-53796c2494/

ようやく気付いたのだが、typescript のコードを生成するということは proto ファイルは実行時にいらないんじゃないか? proto ファイルは情報が載っているだけで、そのファイルを使って gRPC を行うわけではない。proto ファイルが TypeScript でいう型情報みたいなものだと考えて良いと思う。

そう考えれば、typescript もそんなに身構えなくてよいのではなかろうか。構えてないけどね。

 

[typescript] 複数のimportするファイルを何とかしたい

タイトルだと全然わからんのでちゃんと説明をします......

 

発端は gRPC クライアントアプリを JavaScript で作っていたので TypeScript にしようと考えたことだった。
いままで TypeScript のことを調べはしたものの、 1ファイルで終わるような内容であれば JavaScript で済ませていたのだが、大きくなりそうな気配があったので TypeScript にしておこうとしたのだ。

--init で tsconfig.json を作り、ちょっとだけ変更して、拡張子を js から ts にリネームし、プリミティブ型以外のところは any で逃げてコンパイルというかトランスパイルというか、ともかく JavaScript にして node.js で実行するところまではできた。

次の段階は any を何かの型に置き換える作業だろうと思って進めていた。
作っていた JavaScript の中では特に Object を作るようなことはしていなかったのだが、 gRPC のリクエストとレスポンスは proto ファイルに由来する型を使っていた。

では proto ファイルから型を作る何かがあるだろうと調べて出てきたのがこれだった。

Generating TypeScript types
https://github.com/grpc/grpc-node/tree/master/packages/proto-loader#generating-typescript-types

npx proto-loader-gen-types を実行すると、proto ファイルを読み込んで型情報を持つ ts ファイルを作ってくれた。
そこまでは良かったのだが、proto ファイルに記述されている message ごとに ts ファイルを作るようだった。使う予定の proto ファイルにはたくさん message があり、大量の ts ファイルが生成された。

これを import すれば使えそうなのだが、その前に「この大量のファイルを import するということ自体を何とかできんだろうか、と考えたのだ。


まず考えたのがワイルドカード。
import もモジュールといいつつファイル名みたいなもんだから from のところに './proto/*' みたいな書き方をすればよいんじゃないの?と思うのは仕方あるまい。

結果としてダメだった。
まあ、同じディレクトリにあるだけで読み込まれるってのはさすがに危険すぎるな。

 

dynamic import というやり方も見つけたのだが、別に静的で良いのだ。
単に import 文をたくさん書きたくないだけなのだよ。
いや、書いても良いけど import だけで何十行もあるような状況が嫌なのだよ。

 

というわけで次に思いついたのが、別の ts ファイルに import だけ詰め込んで それを import するような手段が執れないだろうか、ということだった。

import - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import

JavaScript だろうと TypeScript だろうと import は import のはずだから MDN の説明でよいと思う。
ただね、読む前に想像すると、そんなことはできないんじゃないかと思うのだ。
まあ、やらんとわからん。

 

image

image

>node index.mjs
aaa=30
bbb=20
ccc=10

TypeScript ではないが、まあよかろう。
これが基本形だ。

 

ダメだったワイルドカード。

import * as aaa from './files/*.js';

console.log(`aaa=${aaa.A}`);
console.log(`bbb=${aaa.B}`);
console.log(`ccc=${aaa.C}`);

Cannot find module になる。
ワイルドカードと見ずに "*.js" というモジュール名として処理するからだろう。

 

a.js, b.js, c.js は変更しないとして files/index.mjs を追加してみた。
まあ、ダメなんだけどね。

image

>node index.mjs
aaa=undefined
bbb=undefined
ccc=undefined

import でモジュール名しか指定していない場合は import はしないのだね。。。

副作用のためだけにモジュールをインポートする
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import#import_a_module_for_its_side_effects_only

それに、もし import できていたとしても export していないから参照できないだろう。
これなら動いた。

image

>node index.mjs
30
20
10

export {a.A} みたいな書き方をすれば total.A で参照できるかと思ったがダメだった。 export {a.A as A} もダメだ。
再export すればいける。

image

export from という書き方もあるそうだ。

export
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/export

image

これくらいだったら bash で機械的に作れるんじゃないかな。

2022/05/08

[golang]go.modの入れ子を理解できなかった

go.mod をずいぶん理解できたんじゃないかと思っていたのだが、入れ子になった場合、つまり サブディレクトリの中にも go.mod がある場合にどうしたらよいのかがわからなかった。

 

なんで入れ子にしようとしたかというと、gRPC 用の proto ファイルを別管理しようとしていたからだ。過去の記事にも同じことを書いたような気がするが、

  • proto ファイルをサーバ側のアプリと同じリポジトリに置いた
  • でも proto ファイルの方はバージョンが変わらずサーバアプリだけ更新されることが多い
  • うーん、ならば go.mod を別にした方がよいのでは?

という思考からだ。

リポジトリごと分ける方が無難なのだろうけど、それはそれでやりすぎな気がしてそうしたのだ。
サーバもクライアントもアプリが小さいなら同じリポジトリに置いてしまってよかったかもしれないが、clone するだけで大ごとになるようなサイズなので「サーバ+proto」「クライアント」の2つに分けている。

と「仕方ない」という感を出そうと思ったが、初回のリリースまでは proto もサーバアプリと同じ go.mod にして、リリースしたらどうせ tag を付けるだろうからバージョン管理で逃げてしまえばよかったと思う。
開発中は tag を付けていないので、go.mod に書くときは commit-id を元にした pseudo-version を使っている。

 

入れ子にしたのはこれだけではなかった。
クライアント側も gRPC にアクセスする部分だけ go.mod を別にしてしまったのだ。
gRPC のテスト用クライアントアプリを別に作ったので、gRPC アクセス部分だけ使い回そうとしたのだ。

そんなこんなして、面倒な構成になってしまった。
実際はこんなにシンプルではなく、もっと大量に import している。

image

こうなったとき、go.mod をどう書いて良いのかが結局分からなかった。
go mod tidy してもエラーが出て、いろいろ書き換えても解消できなくなったのだ。 replace を使ってローカルディレクトリを参照したのも原因かもしれない。

結局あきらめて、クライアントアプリの grpcアクセス部分は go.mod を削除して単にクライアントアプリの 1パッケージとして使うことにした。

image


悔しいからいうわけではないが、この構成に変更してよかったと思う。
そもそも、テストアプリはテストアプリに過ぎないのだから、そのために苦労しすぎるのは馬鹿らしい。
それに、grpc の部分を本格的に別のところでも使いたいならクライアントアプリとは別リポジトリにすべきだろう。サーバが複数あることは考えにくいからセットにしたが、クライアントは種類が複数あっても変ではないのだ。

 

だから構成を変更したことに対しては文句がないのだが、その理由が「できなかった」というのは納得がいかないものだ。
できるけどやらない、くらいにしておきたい。

というわけで、入れ子になった go.mod 環境を作って試していこう(本題)。


その前に、用語の整理をしたい。
パッケージは goファイルの中に package として書くのでわかるのだが、モジュールとかバージョンになるとちゃんと理解できていない。今回だって「go.mod の入れ子」と書いているが、正式な名称じゃないと思う。

Modules, packages, and versions
https://go.dev/ref/mod#modules-overview

モジュール

これは package なんかの集合で、識別はモジュールパスで行われる。
モジュールパスは go.mod に依存関係などと一緒に書く。
モジュールルートディレクトリは go.mod があるディレクトリ。
メインモジュールはちょっと違って、go コマンドを実行したディレクトリに含まれているモジュールを指す。つまりルートディレクトリの下にあるサブディレクトリで go コマンドを実行しても、そこに go.mod が含まれていなければ上にたどって直近にある go.mod のモジュールがメインモジュールになるということだ。

git で管理していて普通に作るなら、モジュールパスは git のリポジトリと同じにするだろう。 git 以外でも同じようなものだろう。
ローカルファイルでも悪くはないと思うのだが、どうなんだろうね。


というわけで、ローカルディレクトリで試していこう。

$ go version
go version go1.18.1 linux/amd64

最近は GOPATH を使わなくてもよくなりつつあるようだが、よくわからんので GOPATH を使う。

$ export GOPATH=`pwd`
$ mkdir bin src
$ cd src
$ mkdir mod1 mod2
$ cd mod1
$ go mod init
$ cd ../mod2
$ go mod init
$ cd ..

これで mod1 と mod2 というモジュールができた。
モジュールパスも mod1 と mod2。

なにか import させて go.mod を賑やかにしたいので、昔作った gogo-test1 を持ってこよう。

$ cd mod1
$ go get github.com/hirokuma/gogo-test1
go: downloading github.com/hirokuma/gogo-test1 v0.0.0-20211121014212-382e4677bbfb
go: github.com/hirokuma/gogo-test1@v0.0.0-20211121014212-382e4677bbfb requires
        github.com/hirokuma/yoshio@v0.0.0: reading github.com/hirokuma/yoshio/go.mod at revision v0.0.0: git ls-remote -q origin in /home/xxxx/golang/pkg/mod/cache/vcs/301146d5f0494aad6678724d644f7711fa7ff362d0bc8683a33b7d297ca8e2c3: exit status 128:
        remote: Repository not found.
        fatal: repository 'https://github.com/hirokuma/yoshio/' not found

なんだこりゃ?と思ったが、 gogo-test1 の go.mod で require github.com/hirokuma/yoshio にしてて、それを replace で github.com/hirokuma/gogo-test2 にしているだけだ。

gogo-test1 を clone して go mod tidy してもエラーにならない。
もしかして replace って import すると使えなくなったりするんだろうか。

依存関係がない gogo-test4 はすんなり go get できた。

$ go get github.com/hirokuma/gogo-test4

go.mod

module mod1

go 1.18

require github.com/hirokuma/gogo-test4 v0.1.0

 

main.go

package main

import (
    "fmt"

    gogo "github.com/hirokuma/gogo-test4"
)

func main() {
    gogo.SetValue(3)
    fmt.Printf("%v\n", gogo.GetValue())
}

$ go run .
3


hirokuma/gogo-test1 がダメな理由だが、なんとなく tag で付けた v3 もよくない感じがする。
gogo-test1/go.mod にあった replace をこちらにも追加したのだが、これはダメだった。

$ go mod tidy
go: errors parsing go.mod:
/home/ueno/golang/src/mod1/go.mod:7: no matching versions for query "v3"

module mod1

go 1.18

replace github.com/hirokuma/yoshio v0.0.0 => github.com/hirokuma/gogo-test2 v0.0.0-20211121012830-b239fb1fd1ae

require github.com/hirokuma/gogo-test1 v3

 

こっちだと go mod tidy できた。

module mod1

go 1.18

replace github.com/hirokuma/yoshio v0.0.0 => github.com/hirokuma/gogo-test2 v0.0.0-20211121012830-b239fb1fd1ae

require github.com/hirokuma/gogo-test1 v0.0.0-20211121014212-382e4677bbfb

 

tag に v0.0.3 という名前で追加しても成功したので、 x.y.z 系の tag を付けておくのが無難ということか。
本題とは関係ないのに時間がかかってしまった。


というところで気付いたが、ローカルディレクトリだとバージョンも何もないので意味が無いのでは・・・?

とりあえずやっておこう。

src/
  mod1/
    go.mod
    main.go
  mod2/
    go.mod
    gogo.go

 

mod2/go.mod

module mod2

go 1.18

mod2/gogo.go

package mod2

import "fmt"

func Gogo() {
    fmt.Printf("mod2 GoGo!\n")
}

 

mod1/go.mod

module mod1

go 1.18

replace github.com/hirokuma/yoshio v0.0.0 => github.com/hirokuma/gogo-test2 v0.0.0-20211121012830-b239fb1fd1ae
replace mod2 => ../mod2

require github.com/hirokuma/gogo-test1 v0.0.0-20211121014212-382e4677bbfb
require mod2 v0.0.0

mod1/main.go

package main

import (
    "fmt"
    "mod2"

    "github.com/hirokuma/gogo-test1/gogo"
)

func main() {
    fmt.Printf("gogo-test1!\n")
    gogo.Gogo()
    mod2.Gogo()
}

$ cd mod1
$ go run .
gogo-test1!
gogo!
mod2 GoGo!

mod2 は replace するから require しなくてもよいかと思ったが、それはダメだった。


ここで、mod2 の下に modmod2 を追加する。

image

 

mod2/modmod2/gogogo.go

package modmod2

import "fmt"

func Gogogo() {
    fmt.Printf("modmod2 GoGo!\n")
}

そして mod1 で使う。

 

mod1/go.mod

module mod1

go 1.18

replace github.com/hirokuma/yoshio v0.0.0 => github.com/hirokuma/gogo-test2 v0.0.0-20211121012830-b239fb1fd1ae
replace mod2 => ../mod2
replace modmod2 => ../mod2/modmod2

require github.com/hirokuma/gogo-test1 v0.0.0-20211121014212-382e4677bbfb
require mod2 v0.0.0
require modmod2 v0.0.0

mod1/main.go

package main

import (
    "fmt"
    "mod2"
    "modmod2"

    "github.com/hirokuma/gogo-test1/gogo"
)

func main() {
    fmt.Printf("gogo-test1!\n")
    gogo.Gogo()
    mod2.Gogo()
    modmod2.Gogogo()
}

$ go run .
gogo-test1!
gogo!
mod2 GoGo!
modmod2 GoGo!

 

なんというか、これだと単にディレクトリの位置が違うというだけで、入れ子になっているのは関係ないことになる。

バージョンのことを除けば、go.mod が入れ子になろうと関係がないということかしら。
いつも親?のモジュール名のサブディレクトリっぽいモジュール名を付けていた(今回なら mod2/modmod2 みたいな)ので go.mod がないときと同じように扱っていたのだが、全然違うモジュール名を付けていてもなんとかなったのかもしれない(どうやってリポジトリと紐付けるのかは知らんが)。

mod2/gogo.go から modmod2.Gogogo() を呼ぶこともできた。

package mod2

import (
    "fmt"
    "modmod2"
)

func Gogo() {
    fmt.Printf("mod2 GoGo!\n")
    modmod2.Gogogo()
}

こちらは mod2/go.mod に追加せずにできた。

mod1/go.mod のように追加しなければならないかと思ったのだが、そこはサブディレクトリという特権なのか?
ディレクトリ名を mod2/modmod2 から mod2/modmodmod2 に変更だけしてみたら、mod1/go.mod の replace したパスが存在しないというエラーになった。

$ go run .
../mod2/gogo.go:5:2: modmod2@v0.0.0: replacement directory ../mod2/modmod2 does not exist

mod1/go.mod の replace を ../mod2/modmodmod2 に変更すると go run できた。
なんで mod2/gogo.go は変更しなくてよかったのだろう?
キャッシュかもしれんと考えて各 go.mod があるディレクトリで go clean -modcache と go clean -cache を実行したが変わらなかった。

まあ、やっぱりサブディレクトリの特権ということか。


そう考えると、あまりサブディレクトリに go.mod を持つ利点よりも、バージョン管理が難しくなる欠点が目立ちそうなので避けたい気もする。

最初に書いた proto のディレクトリに go.mod を付けたとしても、tag をディレクトリごとに付けられるわけでもないので、やるなら v0.0.1-proto みたいにサフィックスを付けるとかになるんだろうか。

バージョンの付け方は go.dev に説明がありそうだったが・・・読む気にならん。

Minimal version selection (MVS)
https://go.dev/ref/mod#minimal-version-selection

 

golang の言語仕様自体は結構シンプルな印象を受けているのだが、 go.mod のことで悩む時間が多いのがきついところだ。なんとなく、バージョン管理にまで口を出す傲慢な言語、っていうイメージになってしまった。まあ Java が出てきたときも「ディレクトリが言語に合わせないといけないなんて!」と思ったので今さらかもしれん。オープンソースとはいえ開発主体があるならその意向に沿ってしまうのは仕方ないだろう。嫌なら自分で開発すれば良いだけなのだから。

 

最後の方になって役に立ちそうな記事を見つけた。

Go のモジュール管理【バージョン 1.17 改訂版】
https://zenn.dev/spiegel/articles/20210223-go-module-aware-mode

ありがたや。