🌟

Typescript では !! と Boolean() が完全に同じ動作ではない

2023/10/25に公開

🌼 はじめに

皆さんは Javascript である値を boolean に変換するときどういう方法も使いますか?よく使われる方法は!!(二重否定・Double negation)か、Boolean()だと思います。

const hello = Boolean("hello"); // true
const world = !!"world" // true

Typescript のハンドブックでもその2つを紹介してます。

https://www.typescriptlang.org/docs/handbook/2/narrowing.html#truthiness-narrowing

You can always coerce values to booleans by running them through the Boolean function, or by using the shorter double-Boolean negation.

いちおう型の観点では、!!を使ったら型がtruefalseになり、Boolean()関数を使ったら型がbooleanになる違いがあります。

const hello = Boolean("hello"); // value は true, 型は boolean
const world = !!"world" // value は true, 型も true

今まで!!Boolean()の違いはそれぐらいだと思っていたので、まあどっちでもいいかなと考えていました。

でも本当はまだ違いがあったのです。

結論: 型ガード(Type Guard)

お忙しい方のために結論から明かします。

!!は型ガードとして機能しますが、Boolean()は型ガードとして機能しないです。

型ガードとは何か

Typescript ユーザーなら以下のようなコードはよく見るでしょう。

function padLeft(padding: number | string, input: string) {
  // この時点の padding の型は number | string
  
  // 型ガードする
  if (typeof padding === "number") {
    // この時点の padding の型は number
    return " ".repeat(padding) + input;
  }
  
  // この時点の padding の型は string
  return padding + input;
}

TypeScript は、JavaScript の if/else、三項演算子、ループ、truthiness チェックなどの実行時の制御フロー構成要素に型分析を重ねて該当型に影響することがあります。

上記のコードで、TypeScript は if 文の typeof padding === "number"型ガードという特別なコードフォームとして理解します。このような特別なチェック(型ガード)を見て、宣言された型よりも具体的な型に絞り込むプロセスを narrowing といいます。

Boolean()が型ガードとして機能しない理由

最初に話した通り、!!は型ガードとして機能しますが、Boolean()は型ガードとして機能しません。

つまり、以下のような現象が起きます。

const printName = (name?: string | null) => {
    if (!!name) console.log(name) // name の型は string
    if (Boolean(name)) console.log(name) // ❗️name の型は string | null | undefined❗️
}

どっちも型ガードしたから if 文の範囲内ではnameの型がstringになると思いましたが、Boolean(name)の場合は型がそのままstring | null | undefinedになってます。

その理由は、この記事を書いてる現時点(2023年10月)ではまだ Typescript で Boolean() の型ガードをサポートしてないからです。

なんと6年前に同じ issue があがってました。

https://github.com/microsoft/TypeScript/issues/16655

いちおう2019年にこの問題を修正する実装がマージされてクローズされましたが、しばらくして再発したため、Issueも再度オープンし、そのまま今に至っているようです。

いつかはサポートするかもしれませんが、いつになるかはわからないです、、(^_^)

いつ何を使うべきか

このような違いがあるとわかった以上、どっちでもいいときとそうでもない時を区別する必要があるでしょう。

boolean に変換したいときはどっちでもOK

型ガードは型の話であって実際の機能には影響しないので、boolean 値に変換したいときはどっちを使っても大差はないと思います。両方 boolean 値に変換してくれます。

例えば、ある配列が要素を一つ以上持っているかどうかの boolean 値を返す関数はどっちを使っても実装できます。

const hasItems = (items: string[]): boolean => {
  // ⭕️
  return Boolean(items.length)
    
  // ⭕️
  return !!items.length
}

では JSX をレンダリングするときはどうでしょう。該当値をそのまま使うなら、!!Boolean()両方大丈夫でしょう。

// ⭕️
const hasProductName = Boolean(productName);

// ⭕️
const hasProductName = !!productName;

return (
  <div>
    {hasProductName && <div>{productName}</div>}
  </div>
);

基本的に boolean 値に変換したいだけで、特に型を絞る必要がない場合はどっちでも良いと思います。

+) 参考までに
airbnb の javascript スタイルガイドではBoolean()を good, !!を best としてるようです。

https://github.com/airbnb/javascript#coercion--booleans

// good
const hasAge = Boolean(age);

// best
const hasAge = !!age;

型ガードしたいときはBoolean()はやめよう

では逆に型を絞る必要がある場合はBoolean()は避けたほうがいいでしょう。

例えば、以下のように型を絞ってその型に対するメソッドを使うときはBoolean()は適切ではありません。

const toUpperCase = (item: string | null | undefined) => {
    // ❌ - 'item' is possibly 'null' or 'undefined' エラーが発生する
    if (Boolean(item)) return item.toUpperCase()
    
    // ⭕️
    if (item) return item.toUpperCase()
    
    // ⭕️
    if (!!item) return item.toUpperCase()
}

JSX のレンダリングにおいても同様です。truthy 判定してからメソッドを使う、もしくはオブジェクトのプロパティにアクセスする場合はBoolean()は向いてないです。

type Product = { name: string };
type ComponentProps = { products?: Product[] };

function Component({ products }: ComponentProps) {
  // ❌ - map のところで 'products' is possibly 'undefined' エラーが発生する
  const hasProducts = Boolean(products?.length);

  // ⭕️
  const hasProducts = !!products?.length;
  
  // ⭕️
  const hasProducts = products && products.length > 0

  return (
    <div>
      {hasProducts &&
        products.map(({ name }) => <span key={name}>{name}</span>)}
    </div>
  );
}

🌷 終わり

みなさんこの違いご存知でしたか?私はそれなりに TypeScript を使ってきたつもりでしたが、今まで気づきませんでした。おそらく私は!!派なので知るきっかけがあまりなかったかもしれません。

ちなみに.filter(Boolean)で null check ができますが、その場合も型は絞れてないです。

// 実際の値は [1, 2] なのに、型は (number | null | undefined)[] のまま
const truthyArray = [1, 2, undefined, null].filter(Boolean);

記事内で添付したissue.filter(Boolean)の型を絞る方法をいくつか議論してるので、試してみるのもいいと思います。

でも Typescript 側でサポートしてくれたら1番楽なので、やってほしいですね。

GitHubで編集を提案

Discussion