STORES 予約 エンジニアの水野です。STORES 予約 の店舗管理画面で利用しているTypeScriptをv4.8からv5.5にアップグレードしたので追加された主な機能をおさらいしようと思います。
satisfies (v4.9)
v4.9で実装されました。
型アノテーションのように型付けしつつ型推論も行う演算子です。 例を見てみましょう。
type Color = 'red' | 'green' | 'blue' const pallet: Record<Color, string | number[]> = { red: [255, 0, 0], green: '#00ff00', blue: '#0000ff', } // pallet.red => string | number[]
pallet.red
の型はstring | number[]
となっています。これはRecord
でアノテーションしている通りの正常な結果ではありますが、pallet.red
には[255, 0, 0]
という値を渡しているのでnumber[]
で推論してほしいというのが正直な気持ちです。satisfies
を使うことでこれが実現できます。
type Color = 'red' | 'green' | 'blue' const pallet = { red: [255, 0, 0], green: '#00ff00', blue: '#0000ff', } satisfies Record<Color, string | number[]> // pallet.red => number[]
satisfies
によってpallet.red
がnumber[]
と推論されました。もちろん間違った型を渡した場合やプロパティに過不足があった場合は型エラーになります。この様に、型の制約と同時に推論を求める場合にsatisfies
は有用です。
またas const
と併用することも可能です。
const Type Parameters (v5.0)
v5.0で実装されました。
型引数に対してas const
と同様の推論を得られるようになります。例えばcolorをマージする関数があったとして戻り値の推論結果は以下のようになります。
const mergeColors = <T extends Record<string, string>>( colors: T ): T & { green: "#00ff00" } => { return { ...colors, green: "#00ff00", }; }; const colors = mergeColors({ red: "#ff0000" }); // colors.green => "#00ff00" // colors.red => string
green
と異なり引数で与えたred
はstring
と推論されています。red
もgreen
と同様リテラル型で推論してほしい場合はas const
を使う方法があります。
const colors = mergeColors({ red: "#ff0000" } as const); // colors.green => "#00ff00" // colors.red => "#ff0000"
望む結果は得られましたがmergeColors
を呼び出す度にas const
が必要となり冗長です。const Type Parametersを使うことこのas const
が不要になります。
// 型引数Tの前にconstを付与する const mergeColors = <const T extends Record<string, string>>( colors: T ): T & { green: "#00ff00" } => { return { ...colors, green: "#00ff00", }; }; const colors = mergeColors({ red: "#ff0000" }); // colors.green => "#00ff00" // colors.red => "#ff0000"
型引数T
の前にconst
を付与しました。こうするとT
はas const
したときと同様の推論がされます。結果、colors.red
はリテラル型で推論され、mergeColors
を呼び出す度に書いていたas const
も不要になっています。
using keyword (v5.2)
v5.2で実装されました。
関数やメソッドのスコープを抜ける直前に任意の処理を実行可能にします。 DBと接続して処理の終わりにCloseするコードを例に見てみます。
const connectDB = () => { const db = new DB() return { db, [Symbol.asyncDispose]: async () => { await db.close(); }, } } const doSomething = async () => { await using conn = connectDB() conn.db.sql('SELECT ...') // ここで db.close()が実行される }
connectDB
の戻り値のオブジェクトにSymbol.asyncDispose (またはSymbol.dispose)
の名で定義した関数を含めます。そして
connectDB
関数をusing keyword
で呼び出すことで親関数のスコープを抜ける直前にSymbol.asyncDispose
関数の処理が走るようになります。
これがusing keyword
を使わない方法だと以下の様に都度db.close()
を書く必要があるかもしれません。
const doSomething = () => { let conn try { conn = connectDB() conn.db.sql('SELECT ....') } finally { conn.db.close() } }
必要だけど漏れやすい処理をカプセル化できる点がusing keyword
の利点です。
唐突にでてきたSymbol.asyncDispose
ですがこれはWell-known symbol
と言われる組み込みのSymbol
でこの他にも様々なSymbolがあります。
NoInfer (v5.4)
v5.4で実装されました。
Infer
は推論するという意味なのでNoInfer
だとその反対の意味になります。Noinfer
を使うと「与えられた情報を推論の材料に使わないでくれ」という意思をコンパイラに伝えることができます。例を見てみましょう。
const createStreetLight = <C extends string>(colors: C[], defaultColor: C) => { // ... } createStreetLight(["red", "yellow", "green"], "blue"); // 型引数C => "red" | "yellow" | "green" | "blue"
createStreetLight
の型引数C
は与えられた引数の値から"red" | "yellow" | "green" | "blue"
と推論されています。正しい結果なので型エラーもありません。
ここで引数defaultColor
の値は引数colors
に渡した値のいずれかに限るという制約を設けたいとします。これはNoInfer
を使うことで実現できます。
const createStreetLight = <C extends string>(colors: C[], defaultColor: NoInfer<C>) => { // ... } // これは型エラーになる // Argument of type '"blue"' is not assignable to parameter of type '"red" | "green" | "yellow"'. createStreetLight(["red", "yellow", "green"], "blue"); // 型引数C => "red" | "yellow" | "green"
defaultColor
の型をNoInfer<C>
としました。こうすることでdefaultColor
に与えられた値を推論の材料に使うなとコンパイラに伝えています。結果、型引数C
は"red" | "yellow" | "green"
と推論され第二引数に与えた"blue"
で型エラーにすることができました。
推論の強化 (v5.5)
v5.5になって推論がより強化されました。
filter
Array
のfilter
メソッドで推論されるようになったのは大きな利点です。これはTypeScriptが関数の内容から型述語を推論するようになったため可能となりました。
const arr = ['foo', 'bar', null].filter((v) => v !== null) // v5.4まで arr => (string | null)[] // v5.5から arr => string[]
しかしこの推論がされるには4つの条件があります。
- 関数の戻り値に明確な型アノテーションや型述語が定義されていないこと
- 関数の中でreturnされていること
- 関数が引数を変更しないこと
- 関数が引数を使ったブール式を返すこと
このため以下のような実装では推論が効かなくなります。
// 関数に明確な型述語が定義してある const isNumber = (v: unknown): v is number => { return typeof v === 'number' } const arr = [1, 2, null].filter(isNumber) // arr => (number | null)[] // キャストしておりブール式を返していない const arr = [1, 2, null].filter((v) => !!v) const arr = [1, 2, null].filter(Boolean) // arr => (number | null)[]
1の条件については、明確な型述語には関数が比較している型と矛盾があってもエラーにならないという問題があったのでその対応の結果だと思います。関数の戻り値の型は定義したい身としてこの仕様は少し悩ましいと感じますが、ワンライナーで書ければ型述語を定義しているのとほぼ同じなので一応の納得はできます。
// const isNumber: (v: unknown) => v is number const isNumber = (v: unknown) => typeof v === 'number'
型ガード
filter
で触れた通り、型述語を推論するようになったので明確な型述語無しで型ガードも効くようになりました。
// v5.4まで型述語が必要 const isNumber = (v: unknown): v is number => { return typeof v === 'number' } // v5.5から型述語がなくても型ガードが効く const isNumber = (v: unknown) => typeof v === 'number' let foo: string | number if (isNumber(foo)) { // foo => number }
正規表現チェック (v5.5)
v5.5で実装されました。
正規表現の誤りをTypeScriptが指摘してくれるようになりました。
// error: Range out of order in character class. const regex = /[a-Z]/ // error: Numbers out of order in quantifier. const regex = /a{2,1}/; // error: There is nothing available for repetition const regex = /^$*+?.()|{}[]/;
おわりに
以上、v4.9からv5.5で追加された主な機能のおさらいでした。こういった新機能は積極的に使って体に慣らしていかないと宝の持ち腐れになってしまいます。使ってみた所感などをチームで共有しながら開発に役立てて行きたいと思います。