🗂️

TypeScriptのWidening・Narrowingについて

に公開

TypeScriptの練習問題を解いている最中、 WideningNarrowing という概念があったので、それぞれについて整理したいと思います。

Widening

Wideningとは、英語で「広がる」という意味を表す widen の現在分詞であり、「拡大」のことを指します。
それでは、型の拡大とはどのような意味なんでしょう?
TSでletconstで変数を定義し、それぞれ値を代入した場合の4パターンの変数の型はこのようになります。

// パターン1: let => let
let value1 = "widening"; // let value1: string
let clone1 = value1; // let clone1: string

// パターン2: let => const
let value2 = "widening"; // let value2: string
const clone2 = value2; // const clone2: string

// パターン3: const => let
const value3 = "widening"; // const value3: "widenging"
let clone3 = value3; // let clone3: string

// パターン4: const => const
const value4 = "widening"; // const value4: "widenging"
const clone4 = value4; // const clone4: "widenging"

この時、パターン3のconstの値をletの値に代入した例を見てみましょう。
constで定義されたvalue3"widening"型で定義されていたにも関わらず、letで定義されたclone3string型で定義されてしまっています。
そのため、clone3は適当な文字列で値を上書きできてしまいます。

// パターン3: const => let
const value3 = "widening"; // const value3: "widenging"
let clone3 = value3; // let clone3: string
clone3 = "hoge"; // ok

このように、"widening"型からstring型になってしまう事象をWidening(型の拡大)と言います。

では、Wideningの解決策は何かというと、変数に対してas constを付与すると、型が拡大せずに済みます。

// パターン3: const => let
const value3 = "widening" as const; // const value3: "widenging"
let clone3 = value3; // let clone3: "widening"
clone3 = "hoge"; // Type '"hoge"' is not assignable to type '"widening"'.

具体的に、どのような場合に気をつけるべきかというと、
定義した変数をexportで参照できるようにした場合、値が変更できてしまう恐れがあるため対策しておきましょう。

Badパターン

constants.ts
export const ROLES = {
  admin: "ADMIN",
  readonly: "READ_ONLY",
}
user.ts
import { ROLES } from 'constants.ts';

ROLES.readonly = "mutable";
console.log(ROLES);
// { admin: "ADMIN", readonly: "mutable" }

Goodパターン

constants.ts
export const ROLES = {
  admin: "ADMIN",
  readonly: "READ_ONLY",
} as const
user.ts
import { ROLES } from 'constants.ts';

ROLES.readonly = "mutable";
// Cannot assign to 'readonly' because it is a read-only property.

たしかに、最近アサインしたチームのコードを見た時に
毎回、as constをしていたのは、このためだったんですね!

Narrowing

Narrowingとは、Wideningの対義語であり、英語で「狭い」という意味を表すnarrowの現在分詞です。
「型を狭くする」という意味は、ある程度想像できるかと思います。
入力値の先頭に空白・文字列を追加する関数があり、引数のpaddingnumberstringを許容している場合、下記のコードだとエラーになってしまいます。

const padLeft = (padding: number | string, input: string): string => {
  return " ".repeat(padding) + input;
  // Argument of type 'string | number' is not assignable to parameter of type 'number'.
  // Type 'string' is not assignable to type 'number'.
}

なので、一般的に型判定の条件分岐を追加するかと思います。
この処理のことをType Narrowingと言います。

const padLeft = (padding: number | string, input: string): string => {
  if (typeof padding === "number") {
    return " ".repeat(padding) + input;
  }
  return padding + input;
}

Type Narrowingは、今までもごく普通に使ってましたね😁
公式サイトには、他にもいくつかの方法が紹介されていたので、参考になるかもしれないです。

Discussion