Template string types and mapped type 'as' clauses 機能説明(意訳)
(これは以下の PR で Anders Hejlsberg 氏が最初にコメントした内容を意訳しただけの個人用メモです)
この PR は以下 2 つの新機能を実装します:
- テンプレート文字列型, ジェネリックなプレースホルダーが埋め込まれた文字列リテラルの形をとり、型インスタンス化を通してプレースホルダが実際の文字列リテラルで置き換えられるような型
- Mapped type
as
節, mapped types においてプロパティ名を変換できるようにする
テンプレート文字列型
テンプレート文字列型はテンプレート文字列式の型版 (the type space equivalent of template string expressions) です。テンプレート文字列式と同様、テンプレート文字列型はバックティックで囲まれ、${T}
の形のプレースホルダを含めることができます。ここで T
は string
, number
, boolean
, または bigint
にアサイン可能な型です。テンプレート文字列型ではリテラル文字列を結合したり、非 string なプリミティブ型をその文字列表現に変換したり、文字列リテラルの大文字小文字を変えることができます。さらに、型推論によって、テンプレート文字列型はシンプルな形式の文字列パターンマッチと分解を行ってくれます。
テンプレート文字列型は以下のように解決されます:
- プレースホルダ内の union 型はテンプレート文字列全体に分配されます。例えば
`[${A|B|C}]`
は`[${A}]` | `[${B}]` | `[${C}]`
に解決します。複数のプレースホルダ内の union 型はクロス積に解決します。例えば`[${A|B},${C|D}]`
は`[${A},${C}]` | `[${A},${D}]` | `[${B},${C}]` | `[${B},${D}]`
に解決します。 - プレースホルダ内の string, number, boolean, そして bigint リテラル型はそのリテラル型の文字列表現でプレースホルダを置き換えます。例えば
`[${'abc'}]`
は`[abc]`
に、`[${42}]`
は`[42]`
に解決します。 - プレースホルダ内に
any
,string
,number
,boolean
, またはbigint
型のうちのいずれかが 1 つでもあれば、そのテンプレート文字列はstring
型に解決します。 - プレースホルダ内に
never
型があれば、そのテンプレート文字列はnever
型に解決します。
例をいくつか:
type EventName<T extends string> = `${T}Changed`; type Concat<S1 extends string, S2 extends string> = `${S1}${S2}`; type ToString<T extends string | number | boolean | bigint> = `${T}`; type T0 = EventName<'foo'>; // 'fooChanged' type T1 = EventName<'foo' | 'bar' | 'baz'>; // 'fooChanged' | 'barChanged' | 'bazChanged' type T2 = Concat<'Hello', 'World'>; // 'HelloWorld' type T3 = `${'top' | 'bottom'}-${'left' | 'right'}`; // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' type T4 = ToString<'abc' | 42 | true | -1234n>; // 'abc' | '42' | 'true' | '-1234'
Union 型をクロス積分配するとあっという間に非常に大きくてコストのかかる型へと拡大する可能性があることに注意してください。また union 型の構成要素は 100,000 未満に制限されることにも注意してください。以下のコードはエラーになります:
type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; type Zip = `${Digit}${Digit}${Digit}${Digit}${Digit}`; // Error
テンプレート文字列プレースホルダは型の前に uppercase
, lowercase
, capitalize
, または uncapitalize
修飾子を任意で指定できます。この修飾子は置き換えられる文字列の全体または先頭文字の大文字小文字を変化させます。例えば:
type GetterName<T extends string> = `get${capitalize T}`; type Cases<T extends string> = `${uppercase T} ${lowercase T} ${capitalize T} ${uncapitalize T}`; type T10 = GetterName<'foo'>; // 'getFoo' type T11 = Cases<'bar'>; // 'BAR bar Bar bar' type T12 = Cases<'BAR'>; // 'BAR bar BAR bAR'
テンプレート文字列型はすべて string
型とそのサブタイプにアサインできます。さらに、テンプレート文字列型 `${T}`
は `${C}`
(ここで C
は T
の文字列リテラル型制約) とそのサブタイプにアサインできます。例えば:
function test<T extends 'foo' | 'bar'>(name: `get${capitalize T}`) { let s1: string = name; let s2: 'getFoo' | 'getBar' = name; }
型推論では文字列リテラル型からテンプレート文字列型への推論をサポートしています。推論がうまくいくためには、推論先となるテンプレート文字列型の中の各プレースホルダが少なくとも1文字のスパンで隔てられていなければなりません(言い換えると、ぴったり隣り合ったプレースホルダたちへと推論することはできません)。各スパンを左から右へと非貪欲マッチを行いそのスパンで隔てられた2つの部分文字列からプレースホルダ型への推論を行うことで推論が進みます。例をいくつか:
type MatchPair<S extends string> = S extends `[${infer A},${infer B}]` ? [A, B] : unknown; type T20 = MatchPair<'[1,2]'>; // ['1', '2'] type T21 = MatchPair<'[foo,bar]'>; // ['foo', 'bar'] type T22 = MatchPair<' [1,2]'>; // unknown type T23 = MatchPair<'[1,2] '>; // unknown type T24 = MatchPair<'[1,2,3,4]'>; // ['1', '2,3,4']
テンプレート文字列型を再帰的 conditional types と組み合わせて、繰り返しパターン上をイテレートする Join
型と Split
型を書くことができます。
type Join<T extends (string | number | boolean | bigint)[], D extends string> = T extends [] ? '' : T extends [unknown] ? `${T[0]}` : T extends [unknown, ...infer U] ? `${T[0]}${D}${Join<U, D>}` : string; type T30 = Join<[1, 2, 3, 4], '.'>; // '1.2.3.4' type T31 = Join<['foo', 'bar', 'baz'], '-'>; // 'foo-bar-baz' type T32 = Join<[], '.'>; // ''
type Split<S extends string, D extends string> = string extends S ? string[] : S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] : [S]; type T40 = Split<'foo', '.'>; // ['foo'] type T41 = Split<'foo.bar.baz', '.'>; // ['foo', 'bar', 'baz'] type T42 = Split<any, '.'>; // string[]
再帰的推論機能は、例えば、プロパティにドット繋ぎでアクセスする関数や、 JavaScript フレームワークでたまに使われるパターンを強く型付けするのに使えます。
type PropType<T, Path extends string> = string extends Path ? unknown : Path extends keyof T ? T[Path] : Path extends `${infer K}.${infer R}` ? K extends keyof T ? PropType<T[K], R> : unknown : unknown; declare function getPropValue<T, P extends string>(obj: T, path: P): PropType<T, P>; declare const s: string; const obj = { a: { b: {c: 42, d: 'hello' }}}; getPropValue(obj, 'a'); // { b: {c: number, d: string } } getPropValue(obj, 'a.b'); // {c: number, d: string } getPropValue(obj, 'a.b.d'); // string getPropValue(obj, 'a.b.x'); // unknown getPropValue(obj, s); // unknown
Mapped type as
節
この PR により, mapped types はオプションの as
節をサポートします。 as
節を通して、生成されるプロパティ名の変換方法を指定できます:
{ [P in K as N]: X }
ここで N
は string
型にアサイン可能な型でなければなりません。 通常、N
は、P
をプレースホルダ内で利用するテンプレート文字列型のような、P
を変換した型です。例えば:
type Getters<T> = { [P in keyof T & string as `get${capitalize P}`]: () => T[P] }; type T50 = Getters<{ foo: string, bar: number }>; // { getFoo: () => string, getBar: () => number }
上記において、keyof T & string
の交差が必要です。なぜなら keyof T
はテンプレート文字列型を使って変換できない symbol
型を含むことがあるからです。
as
節内で指定された型が never
に解決した場合、その key のプロパティは生成されません。したがって、as
節はフィルターとして使えます:
type Methods<T> = { [P in keyof T as T[P] extends Function ? P : never]: T[P] }; type T60 = Methods<{ foo(): number, bar: boolean }>; // { foo(): number }
as
節内で指定された型がリテラル型の union に解決した場合、同じ型の複数のプロパティが生成されます:
type DoubleProp<T> = { [P in keyof T & string as `${P}1` | `${P}2`]: T[P] } type T70 = DoubleProp<{ a: string, b: number }>; // { a1: string, a2: string, b1: number, b2: number }
Fixes #12754.
Playground: https://www.typescriptlang.org/play?ts=4.1.0-pr-40336-88
zsh の check-for-changes をやめて check-for-staged-changes を使う
zsh のプロンプトに現在いる git リポジトリの状態を表示させたいと思って、今までいくつかの設定を .zshrc に書いていたのだけど、ある巨大な git リポジトリを clone してきたディレクトリ内に限り、 cd
コマンドを打ったり、そもそも何もコマンドを打たずに Enter キーを押しただけでも、結果が返ってくるまでにそこそこの頻度で 1 分ぐらいかかってマシンのファンが凄い勢いで回り始めるという現象が起きるようになってしまった。
それで今まで騙し騙し使っていたのを今日ちょっとさすがに原因を調べようと思っていろいろ見ていたところ「 check-for-changes
は重い」というような記述を見つけた。
そこで man zshcontrib
を読んでみると
check-for-changes
If enabled, this style causes the %c and %u format escapes to show when the working directory has uncommitted changes. The strings displayed by these escapes can be controlled via the stagedstr and unstagedstr styles. The only backends that currently support this option are git, hg, and bzr (the latter two only support unstaged). For this style to be evaluated with the hg backend, the get-revision style needs to be set and the use-simple style needs to be unset. The latter is the default; the former is not. With the bzr backend, lightweight checkouts only honor this style if the use-server style is set. Note, the actions taken if this style is enabled are potentially expensive (read: they may be slow, depending on how big the current repository is). Therefore, it is disabled by default.
特に最後の一文の Note に
the actions taken if this style is enabled are potentially expensive (read: they may be slow, depending on how big the current repository is). Therefore, it is disabled by default.
と明確に書かれていた。
check-for-changes
を無効にすることも選択肢の一つとしてはあるのだろうけど、やっぱり未 commit の変更が成されたファイルが無いか、そういう dirty な状態でないかは表示させたいよな…と思ったところ、すぐ下に
check-for-staged-changes
This style is like check-for-changes, but it never checks the worktree files, only the metadata in the .${vcs} dir. Therefore, this style initializes only the %c escape (with staged‐ str) but not the %u escape. This style is faster than check-for-changes. In the git backend, this style checks for changes in the index. Other backends do not currently implement this style. This style is disabled by default.
という値の説明があった。つまり check-for-changes
では staging エリアだけでなく作業ディレクトリ内に変更があるかどうかもチェックするが、 check-for-staged-changes
なら staging エリアに変更されたファイルがあるかどうかだけをチェックするので check-for-changes
よりも速い、ということだ。
そこで
zstyle ':vcs_info:git:*' check-for-changes true
と設定していたのを
zstyle ':vcs_info:git:*' check-for-staged-changes true
と書き換えた。すると結果が返ってくるまでに 10 秒ぐらいかかる時もあるものの、待ち時間はずいぶん改善された。
今までプロンプトに表示させていた情報が 1 つ落ちるのでまだちょっと心もとないが、まあ現実的に妥当な落とし所ではあるかな…
もう少しこのまま使って様子を見ようと思う。
PostgreSQLを起動するためのdocker-compose.yml
この docker-compose.yml
を書く:
version: "3.8" services: db: image: postgres:12.3 restart: always volumes: - db-data:/var/lib/postgresql/data environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: password volumes: db-data:
いまさら何をという感じだが、上記の docker-compose.yml
を先日書こうとして、いつもフンイキで触っている Docker だけどそろそろドキュメントを読んでおくかと思い立ち、ドキュメントを頭から(飛ばし飛ばし)読んだのでメモを残したくなりました。
環境
% docker --version Docker version 19.03.12, build 48a66213fe % docker-compose --version docker-compose version 1.26.2, build eefe0d31
Docker のアーキテクチャ
https://docs.docker.com/get-started/overview/#docker-architecture
Docker uses a client-server architecture. The Docker client talks to the Docker daemon, which does the heavy lifting of building, running, and distributing your Docker containers.
Docker はクライアント-サーバモデルで動いており、 Docker クライアントが Docker デーモンと会話する。Docker デーモンが Docker コンテナたちをビルドしたり実行したり配布したりといった何やかんや大変な仕事をやってくれる。
Docker デーモン
The Docker daemon (
dockerd
) listens for Docker API requests and manages Docker objects such as images, containers, networks, and volumes.
Docker デーモンは Docker API リクエストを listen して、イメージ、コンテナ、ネットワーク、ボリュームといった Docker オブジェクトたちの面倒を見る。
イメージ
An image is a read-only template with instructions for creating a Docker container.
イメージはコンテナを作るための指示が書かれた読み取り専用のテンプレート。
ubuntu
イメージとか redis
イメージとか。
コンテナ
A container is a runnable instance of an image.
コンテナはイメージの実行可能なインスタンス。
When a container is removed, any changes to its state that are not stored in persistent storage disappear.
コンテナが remove されると、コンテナの状態に対して成された変更のうち永続ストレージに保存されていないものは消滅する。
サービス
Services allow you to scale containers across multiple Docker daemons, which all work together as a swarm with multiple managers and workers.
サービスにより複数の Docker デーモンにわたってコンテナをスケールできるようになる。 Docker デーモンたちは複数のマネージャとワーカーからなる1つの群れとして協働する。
データの永続化について
上で引用したように
When a container is removed, any changes to its state that are not stored in persistent storage disappear.
となっているわけで、コンテナがいなくなった後でも消えてほしくないアプリケーションデータはどう保持しておけば良いのかと思うのだが、ドキュメントを読んでいくと volumes を作りなさいと書かれている
(というか volumes を使うことが推奨されている。他にも bind mounts と tmpfs
mounts という手段があるがここでは割愛する)。
例えば https://docs.docker.com/develop/dev-best-practices/#where-and-how-to-persist-application-data に書かれているように:
- Avoid storing application data in your container’s writable layer using storage drivers. This increases the size of your container and is less efficient from an I/O perspective than using volumes or bind mounts.
- Instead, store data using volumes.
詳しくは https://docs.docker.com/storage/ 以下のドキュメントを読むとよい。
Docker Compose
https://docs.docker.com/compose/
Compose is a tool for defining and running multi-container Docker applications.
Docker Compose は複数コンテナからなる Docker アプリを定義して実行するためのツールである。
docker-compose.yml
の中で services:
というトップレベルのキーの下に各サービスを定義する。
version について
https://docs.docker.com/compose/compose-file/compose-versioning/#versioning
docker-compose.yml
の書式のバージョンを指定することができる。
There are currently three versions of the Compose file format:
- Version 1, the legacy format. This is specified by omitting a
version
key at the root of the YAML.- Version 2.x. This is specified with a
version: '2'
orversion: '2.1'
, etc., entry at the root of the YAML.- Version 3.x, the latest and recommended version, designed to be cross-compatible between Compose and the Docker Engine’s swarm mode. This is specified with a
version: '3'
orversion: '3.1'
, etc., entry at the root of the YAML.
ファイルの先頭に version
キーを 書かない ことでバージョン1を指定したことになる。
バージョン 2.x と 3.x の指定はファイルの先頭に version: '2'
や vesion: '2.1'
や version: '3'
や vesion: '3.1'
などと書くことで指定できる。
推奨バージョンは現行の 3.x である。
また Note が書かれていて
Note: When specifying the Compose file version to use, make sure to specify both the major and minor numbers. If no minor version is given,
0
is used by default and not the latest minor version.
利用する Compose ファイルのバージョンを指定する際は、メジャー ナンバーと マイナー ナンバーの両方を忘れずに指定すること。マイナーバージョンが指定されないと、最新のマイナーバージョンではなく 0
がデフォルトで使われる。
つまり version: "3"
と書くとこれは version: "3.0"
と書いたのと等価である。このような仕様なので、しがらみが何もなければ常に最新のマイナーバージョンを明示的に指定するのが良いだろうと思う。
image
https://docs.docker.com/compose/compose-file/#image
Specify the image to start the container from. Can either be a repository/tag or a partial image ID.
コンテナを開始する起点となるイメージを指定する。
PostgreSQL なら image: postgres
とか image: postgres:12.3
とかを指定する:
https://hub.docker.com/_/postgres
volumes
上でも書いたように volumes を作ってデータを永続化させないと DB としての役目を果たすことができない。
docker-compose.yml
で volumes を定義する方法はこちらに載っている:
https://docs.docker.com/compose/compose-file/#volumes
Mount host paths or named volumes, specified as sub-options to a service.
volumes は各 service のサブオプションとして指定し、ホストの path または名前つき volumes (named volumes) をマウントする。
But, if you want to reuse a volume across multiple services, then define a named volume in the top-level
volumes
key.
ある volume を複数 service 間で使い回したかったら、トップレベルの volumes
キーの下に名前付き volume を定義して使う。
(ドキュメントを読んでみてもこのトップレベル volumes
以外の場所で named volumes を定義できるのかどうかはよくわからなかった。)
トップレベル volumes
の下の各エントリは、最初に載せた docker-compose.yml
の例のように空っぽでも良いのだけど、オプションを指定することもできる(空っぽの場合はデフォルトの値が適用されるということ)。オプションについて詳しくはこちらを読むと良い:
https://docs.docker.com/compose/compose-file/#volume-configuration-reference
restart
postgres の docker image のREADME.md に載っていたのをそのまま書いたという以上の経験が無い。
ドキュメントへのリンクだけ載せておく: https://docs.docker.com/compose/compose-file/#restart
environment
https://docs.docker.com/compose/compose-file/#environment
環境変数を指定する。
ちなみに
Environment variables with only a key are resolved to their values on the machine Compose is running on, which can be helpful for secret or host-specific values.
キーだけの環境変数が書かれていると Compose が実行されたそのマシンのシェルの環境変数の値に解決される。これは秘匿情報やホストに特有の値を扱うときに便利。
この情報は Reference だけでなく Product Manuals でも紹介されている:
https://docs.docker.com/compose/environment-variables/#pass-environment-variables-to-containers
postgres
postgres の事情で言うと POSTGRES_PASSWORD
の指定だけは必須である:
POSTGRES_PASSWORD
環境変数は PostgreSQL のスーパーユーザのパスワードとなる。
また POSTGRES_USER
はスーパーユーザの名前を設定する環境変数で、特に指定されないとデフォルトでは postgres
となる。
なので最初に載せた docker-compose.yml
の例の中では書かなくても同じことだけど、個人的に明示的に書くほうが好きなので書いている。
おわりに
このようにして最初に挙げた docker-compose.yml
が書けた。
たぶん適当に検索すれば数分で同じ内容が書けただろうとは思うけど一次情報にあたってアーキテクチャに対するメンタルモデルを醸成したり version
や environment
に関する tips を手に入れたりすることも大事ですね。