反応する.ReactNode型はブラックホールです


開発者として、我々はいくつかの異なる理由のためにtypescriptを使用します.自己ドキュメンテーションの側面は巨大である-は、なじみのない機能にステップすることができ、それが期待しているオブジェクトの形状を知っている大規模なプロジェクトに取り組んで大規模な恩恵です.との追加のツールの機能IntelliSense そして、そのILKはまた、生産性のための大きな助けです.しかし、私にとって、強くタイプされたシステムを使用する最も重要な理由はランタイムのバグの全部のクラスを除去することです.
それは、このポストの目的につながる最後の理由です.私は最近、反応コンポーネントが実行時に例外をスローしていたバグを処理しました.問題の原因は、我々のアプリケーションのこの領域を国際化する際に行われた最近のリファクタであったReact.ReactNode 誤ってクラスのオブジェクトを通過したTranslatedText これはレンダリングできませんでした.
これは正確には、スクリプトがコンパイル時にキャッチすると予想するバグです.
どうやって起きたの?高いレベルで、それはそうですReact.ReactNode に含まれるタイプDefinitelyTyped , 世界中の何百ものコードベースで使用されるので、実質的に無意味であるために、弱く定義されます.
我々は、2008年のTILセグメントの間、これを高レベルで議論しましたJS Party #213 , しかし、私はそれがより厳しい処置に値すると思いました.
私が探索を共有するように、なぜこのバグが3以上の野生の中で残っているか年since it was originally reported , そして、どのように我々は再び自分自身を保護するために我々のCodeBaseでそれを回避しました.

状況
簡単なバグレポートから始めました.
When I click on "Boost nudges" and attempt to select a filter group, I get an error saying something went wrong. This feature is vital for a demo I have tomorrow.
私の最初のチェックは、私が生産アプリケーションでそれを再現できるかどうかを確認することでした.そうでした.次に開発者環境を起動し、有用なバックトレースを得ることができました.

解釈:反応はそれがレンダリングできない何かをレンダリングしようとしていた.ファイルと行番号を使用して、より多くを追跡するために、私は問題のオブジェクトが呼ばれる支柱であるのを見ることができましたdescription 次の型定義を使用します.
description: string | React.ReactNode;
代わりに呼び出し元がTranslatedText オブジェクトは、国際化を処理するために使用するクラスです.期待される用途は、このオブジェクトが<T> 使用する方法を知っているコンポーネントと、現在のユーザーの正しい言語でテキストをレンダリングする文字列のライブラリ.
これを見た:フィックスは超簡単だった.ラップするTranslatedText オブジェクトを<T> プロップとして渡す前のコンポーネント.

代わりにこのパッチで、即座のバグは解決されました、そして、デモでチケットに記載されているデモは妨げられませんでした.
どのようにバグを理解する超簡単だった-このアプリケーションのこの部分は最近、国際化されていたバグは、その作品で導入されました.しかし、本当のパズルを開始:このタイプのバグを正確にどのようなタイプスクリプトとタイプを使用して防ぐことになっていますか?どのように世界では、タイプシステムは、タイプで小道具に渡される反応によってレンダーブルではなかった何かを許したstring | React.ReactNode ?


私が最初にこの問題が捕らえられていなかったのを見たとき、私の初期の考えは何らかの理由でタイプチェックが全く走らないことであったかもしれませんでした.多分、私たちはクロスモジュール呼び出しでバグを持っていました.しかし、私はすぐにこれを支配することができましたstring そして、それがタイプエラーを引き起こしたのを見ます.
私が試した次のことは、どうにかして見るためのテストだったTranslatedText どうにかしてReact.ReactNode インターフェースですが、クイックimplements (訳注)class TranslatedText implements React.ReactNode ) コンパイラはエラーをスローしました.それは私の期待にマッチしました、なぜなら、それはインターフェースを実装しないからです.
それから、私はそれをReact.ReactNode が定義されている.これらの定義はDefinitelyTyped , 型をネイティブに含んでいないNPMパッケージの型定義の標準的なオープンソースリポジトリkey definitions 以下のようになります.
    type ReactText = string | number;
    type ReactChild = ReactElement | ReactText;

    interface ReactNodeArray extends Array<ReactNode> {}
    type ReactFragment = {} | ReactNodeArray;
    type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
そこにはReactFragment 定義!
The ReactFragment , これはReactNode 型、空のインターフェイスが含まれます.のためthe way that TypeScript handles excess property checks , これはReactNode 型はオブジェクトリテラル以外のオブジェクトを受け入れます.ほとんどすべての目的と目的のために、それは機能的にany 種類このタイプを使用しているほとんどの関数は、“反応によってレンダリング可能な何か”を意味することを期待します.
私はこの時点で私たちのチームにこれをもたらしたHumu :

我々のチームメンバーの1人で掘られる人々がこれがそうであったということを発見したのでknown issue since 2018 ! があるa discussion それは、問題を解決する意図を意味しますが、修正を導入することの波及効果に関する懸念、そして、1年のより良い部分のために進展はありません.

最初の試み
CodeBaseでこの問題に対処する方法を見始めました.
  • CodeBaseのすべてをカスタム型に移動する
  • 使用patch-package 反応を更新します.ノードの定義
  • これらの異なるアプローチの長所と短所を評価することpatch-package アプローチはより少ないコード変化と継続的な認知負荷を必要とするでしょうが、追加の依存関係(および過渡的な依存関係)を必要として、おそらくそれが何が起こっているかをより目に見えないようにする欠点があります.
    結局、我々は試してみることにしたpatch-package 最初に、それがより少ない仕事であるので.変更は超簡単だった我々はパッチを試みたReactFragment 定義された議論スレッドで提案されたものと非常によく似ているタイプ:
    type Fragment = {
      key?: string | number | null;
      ref?: null;
      props?: {
        children?: ReactNode;
      };
    }
    
    このアプローチは、私たちのコードベース内の任意の内部のタイピング問題を引き起こしませんでした、そして、最初に我々を噛ませた誤りのクラスを捕えることができるタイプシステムに終わりました、それはいくつかの反応する生態系図書館に呼び出しでタイプ・エラーをカスケード化させました.我々は我々のコードのインターフェースでトラブルに巻き込まれたreact-beautiful-dnd :

    ウサギの穴を下にダイビングした後、少しの間、これらのタイプの問題を把握しようとすると、より多くのタイプの課題ですべての変更結果を持っているだけで、私はこれは私を理解するよりもより多くのtypescriptのチョップで誰かを必要とすることを決めた.


    第二のアプローチ
    私たちが試みた2つ目のアプローチは、私たちのcodebaseでより厳密なタイプをつくって、見つけて/交換して、至る所でそれを使用して、それから使用されるのを防ぐためにリンターを加えました.私たちが終えたタイプファイルは、パッチアプローチで試したものと非常に似ていました.
    import { ReactChild, ReactPortal, ReactNodeArray } from 'react';
    
    export type StrictReactFragment =
      | {
          key?: string | number | null;
          ref?: null;
          props?: {
            children?: StrictReactNode;
          };
        }
      | ReactNodeArray;
    export type StrictReactNode =
      | ReactChild
      | StrictReactFragment
      | ReactPortal
      | boolean
      | null
      | undefined;
    
    この型が実際に、私たちが予防しようとしていたタイプ誤りのタイプを捕えたと確認した後に、それは我々のコードベースで交換をする時間でした.
    私は簡単にjscodeshift 置換を自動的に行う.私はその道を降り始めました、しかし、私はJscodeshiftを使っている事前の経験がありません、そして、それはトリッキーに証明されました.私が限られた時間を持っていたので、私たちのコードベースが十分に小さいと決めました.
    注:誰かがこのcodemodを書いて、私にそれを送って欲しいならば、私はあなたにShoutoutでこのポストへの付録としてそれを含めて満足です!
    その後のPRでは、より安全なコードベースを使用しましたStrictReactNode 至る所で、しかし、この持続可能にする1つのステップが、ありました.

    Eslintプラグインの作成
    理由React.ReactNode 我々のcodebaseに浸透していたことは、それが多くの状況で使用するそのような論理タイプであるということです.あなたがプロップが反応によってレンダラブルであると断言したいときはいつでもReact.ReactNode .
    今、我々はすべての開発者の代わりにStrictReactNode . これを開発者の裁量に任せるか、マニュアルコードのレビューや教育の一部であることを必要としているのは、特にhumuのような急速に成長している会社ではありえません.
    新しい練習を実施し、それを私たちのコードベースを最新かつ安全に保つためにシームレスにするために、私たちはReact.ReactNode そして、好ましい型への指針でエラーを投げてください.
    この記事はeslintプラグインの動作についてではありませんが、ここで使用したい場合は以下のようにします.
    module.exports = {
        create(context) {
            return {
                TSTypeReference(node) {
                    if (
                        node.typeName.type === 'TSQualifiedName' &&
                        node.typeName.left.name === 'React' &&
                        node.typeName.right.name === 'ReactNode'
                    ) {
                        context.report(
                            node,
                            node.loc,
                            'React.ReactNode considered unsafe. Use StrictReactNode from humu-components/src/util/strictReactNode instead.',
                        );
                    }
                },
            };
        },
    };
    
    もし誰かが事故でやるならReact.ReactNode 型宣言では、以下のようなエラーが発生します:

    Lintingは任意のブランチがマージされる前に発生するCIのテストの一部です.これにより、誰もが誤ってReact.ReactNode 代わりに置換型にします.
    更新:書き込みmore generalized eslint plugin with a fixer !

    ラッピング
    私の観点から、タイプスクリプトとタイプ・システムを使用することの全体のゴールはバグの全部のクラスを防止することができて、この安全を遂行するオリジナルのもののようなリファクタを作ることができます.
    超よく使われるライブラリでこのような広いオープンタイプを持つことは、非常に怖いです.時間が許せば、私はdefinitelytypedでこのパッチを得ることに関して働き続けます、しかし、生態系問題はこれがタイムリーに起こる見込みがないほど大きいです.この大きさの変化は、更新される必要がある波紋とタイプの大きい波をつくります.
    一方、私は非常に我々のようなアプローチを使用することをお勧めしますStrictReactNode コードベースを保護するには