メインコンテンツへスキップ

トラブルシューティングハンドブック:型

⚠️ TypeScript FAQ を読みましたか? 答えがそこにあるかもしれません!

奇妙な型エラーに直面していますか? あなただけではありません。 これは、ReactでTypeScriptを使用する際の最も難しい部分です。 結局のところ、新しい言語を学習しているので、辛抱強く学びましょう。 ただし、これが上手になればなるほど、コンパイラと戦う時間が少なくなり、コンパイラがあなたのために働くようになります!

TypeScriptのメリットを最大限に享受するために、できるだけany型での型指定は避けましょう。 代わりに、これらの問題を解決するためによく使用される戦略をいくつか紹介します。

共用体型と型ガード

共用体型は、これらの型指定の問題のいくつかを解決するのに便利です

class App extends React.Component<
{},
{
count: number | null; // like this
}
> {
state = {
count: null,
};
render() {
return <div onClick={() => this.increment(1)}>{this.state.count}</div>;
}
increment = (amt: number) => {
this.setState((state) => ({
count: (state.count || 0) + amt,
}));
};
}

TypeScript Playgroundで表示

型ガード:共用体型はある領域の問題を解決しますが、別の領域で問題を引き起こす場合があります。 ABが両方ともオブジェクト型の場合、A | Bは「AまたはB」ではなく、「AまたはBまたは両方」であり、前者であると予想した場合に混乱を招きます。 チェック、ガード、およびアサーションの記述方法を学びましょう(以下の条件付きレンダリングセクションも参照)。 例えば

interface Admin {
role: string;
}
interface User {
email: string;
}

// Method 1: use `in` keyword
function redirect(user: Admin | User) {
if ("role" in user) {
// use the `in` operator for typeguards since TS 2.7+
routeToAdminPage(user.role);
} else {
routeToHomePage(user.email);
}
}

// Method 2: custom type guard, does the same thing in older TS versions or where `in` isnt enough
function isAdmin(user: Admin | User): user is Admin {
return (user as any).role !== undefined;
}

TypeScript Playgroundで表示

方法2はユーザー定義型ガードとも呼ばれ、読みやすいコードに非常に役立ちます。 これは、TS自体がtypeofinstanceofで型を絞り込む方法です。

代わりにif...elseチェーンまたはswitchステートメントが必要な場合は、「そのまま動作する」はずですが、ヘルプが必要な場合は弁別共用体型を調べてください。 (Basaratの記事も参照)。 これは、useReducerまたはReduxのreducerの型指定に便利です。

オプショナル型

コンポーネントにオプションのプロパティがある場合は、疑問符を追加し、分割代入中に代入します(またはdefaultPropsを使用します)。

class MyComponent extends React.Component<{
message?: string; // like this
}> {
render() {
const { message = "default" } = this.props;
return <div>{message}</div>;
}
}

!文字を使用して、何かがundefinedではないことをアサートすることもできますが、これは推奨されません。

何か追加することはありますか? 提案とともに issue を提出してください

列挙型

可能な限り列挙型の使用は避けることをお勧めします.

列挙型には、いくつかの文書化された問題があります(TSチームは同意しています)。 列挙型のより簡単な代替方法は、文字列リテラルの共用体型を宣言することです

export declare type Position = "left" | "right" | "top" | "bottom";

列挙型を使用する必要がある場合は、TypeScriptの列挙型はデフォルトで数値であることに注意してください。 通常は、代わりに文字列として使用します

export enum ButtonSizes {
default = "default",
small = "small",
large = "large",
}

// usage
export const PrimaryButton = (
props: Props & React.HTMLProps<HTMLButtonElement>
) => <Button size={ButtonSizes.default} {...props} />;

型アサーション

TypeScriptよりも自分が使用している型の方が狭いことを知っている場合や、他のAPIで動作させるために共用体型をより具体的な型にアサートする必要がある場合があります。そのため、asキーワードでアサートします。 これは、コンパイラよりも自分がよく知っていることをコンパイラに伝えます。

class MyComponent extends React.Component<{
message: string;
}> {
render() {
const { message } = this.props;
return (
<Component2 message={message as SpecialMessageType}>{message}</Component2>
);
}
}

TypeScript Playgroundで表示

何でもアサートできるわけではありません。基本的には、型の絞り込みにのみ使用されます。 したがって、型を「キャスト」することと同じではありません。

プロパティにアクセスするときに、プロパティがnullではないことをアサートすることもできます

element.parentNode!.removeChild(element); // ! before the period
myFunction(document.getElementById(dialog.id!)!); // ! after the property accessing
let userID!: string; // definite assignment assertion... be careful!

もちろん、アサートする代わりに、実際にnullケースを処理するようにしてください :)

名目型のシミュレーション

TSの構造的型指定は、不便になるまでは便利です。 ただし、type brandingを使用して名目型指定をシミュレートできます

type OrderID = string & { readonly brand: unique symbol };
type UserID = string & { readonly brand: unique symbol };
type ID = OrderID | UserID;

コンパニオンオブジェクトパターンを使用してこれらの値を作成できます

function OrderID(id: string) {
return id as OrderID;
}
function UserID(id: string) {
return id as UserID;
}

これで、TypeScriptは間違った場所で間違ったIDを使用することを許可しなくなります

function queryForUser(id: UserID) {
// ...
}
queryForUser(OrderID("foobar")); // Error, Argument of type 'OrderID' is not assignable to parameter of type 'UserID'

将来、uniqueキーワードを使用してブランド化できます。 このPRを参照してください

共通部分型

2つの型を組み合わせることが便利な場合があります。たとえば、コンポーネントがbuttonのようなネイティブコンポーネントのプロパティをミラーリングすることになっている場合などです。

export interface PrimaryButtonProps {
label: string;
}
export const PrimaryButton = (
props: PrimaryButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>
) => {
// do custom buttony stuff
return <button {...props}> {props.label} </button>;
};

Playgroundはこちら

共通部分型を使用して、類似したコンポーネントのプロパティの再利用可能なサブセットを作成することもできます

type BaseProps = {
className?: string,
style?: React.CSSProperties
name: string // used in both
}
type DogProps = {
tailsCount: number
}
type HumanProps = {
handsCount: number
}
export const Human = (props: BaseProps & HumanProps) => // ...
export const Dog = (props: BaseProps & DogProps) => // ...

TypeScript Playgroundで表示

共通部分型(and演算)と共用体型(or演算)を混同しないでください。

共用体型

このセクションはまだ書かれていません(貢献してください!)。 一方、共用体型のユースケースに関する解説を参照してください。

ADVANCEDチートシートには、弁別共用体型に関する情報も含まれています。これは、TypeScriptが期待どおりに共用体型を絞り込んでいない場合に役立ちます。

関数型のオーバーロード

特に関数に関しては、共用体型ではなくオーバーロードが必要になる場合があります。 関数型の最も一般的な記述方法は、省略形を使用します

type FunctionType1 = (x: string, y: number) => number;

しかし、これではオーバーロードを行うことはできません。 実装がある場合は、functionキーワードを使用して、それらを互いに続けて配置できます

function pickCard(x: { suit: string; card: number }[]): number;
function pickCard(x: number): { suit: string; card: number };
function pickCard(x): any {
// implementation with combined signature
// ...
}

ただし、実装がなく、単に.d.ts定義ファイルを作成している場合は、これも役に立ちません。 この場合は、省略形を避け、昔ながらの方法で記述できます。 ここで覚えておくべき重要なことは、TypeScriptに関する限り、関数はキーのない呼び出し可能なオブジェクトにすぎないということです

type pickCard = {
(x: { suit: string; card: number }[]): number;
(x: number): { suit: string; card: number };
// no need for combined signature in this form
// you can also type static properties of functions here eg `pickCard.wasCalled`
};

実際のオーバーロードされた関数を実装する場合、実装は処理する結合された呼び出しシグネチャを宣言する必要があることに注意してください。これは推測されません。 DOM API、たとえばcreateElementでオーバーロードの例を簡単に見ることができます。

ハンドブックのオーバーロードの詳細を読んでください。

推論型の使用

TypeScriptの型推論に頼るのは素晴らしいことです...推論された型が必要であることに気づき、再利用のためにエクスポートできるように型/インターフェースを明示的に宣言する必要があるまでです。

幸いなことに、typeofを使用すれば、それを行う必要はありません。 任意の値で使用してください

const [state, setState] = useState({
foo: 1,
bar: 2,
}); // state's type inferred to be {foo: number, bar: number}

const someMethod = (obj: typeof state) => {
// grabbing the type of state even though it was inferred
// some code using obj
setState(obj); // this works
};

部分型の使用

Reactでは、状態とプロパティのスライスを扱うのが一般的です。 再び、Partialジェネリック型を使用する場合、型を明示的に再定義する必要はありません。

const [state, setState] = useState({
foo: 1,
bar: 2,
}); // state's type inferred to be {foo: number, bar: number}

// NOTE: stale state merging is not actually encouraged in useState
// we are just demonstrating how to use Partial here
const partialStateUpdate = (obj: Partial<typeof state>) =>
setState({ ...state, ...obj });

// later on...
partialStateUpdate({ foo: 2 }); // this works
Partialを使用する際の注意点

現在の動作どおりにPartialを使用することに同意しないTSユーザーがいることに注意してください。 上記の例の微妙な落とし穴はこちらを参照し、@types/reactがPartialではなくPickを使用する理由に関するこの長い議論を確認してください。

必要な型がエクスポートされていませんでした!

これは面倒ですが、型を取得する方法は次のとおりです。

  • コンポーネントのプロパティ型の取得:React.ComponentPropstypeofを使用し、必要に応じて重複する型をOmitします
import { Button } from "library"; // but doesn't export ButtonProps! oh no!
type ButtonProps = React.ComponentProps<typeof Button>; // no problem! grab your own!
type AlertButtonProps = Omit<ButtonProps, "onClick">; // modify
const AlertButton = (props: AlertButtonProps) => (
<Button onClick={() => alert("hello")} {...props} />
);

ComponentPropsWithoutRef(ComponentPropsの代わりに)とComponentPropsWithRef(コンポーネントが特にrefを転送する場合)も使用できます

  • 関数の戻り値の型の取得:ReturnTypeを使用します
// inside some library - return type { baz: number } is inferred but not exported
function foo(bar: string) {
return { baz: 1 };
}

// inside your app, if you need { baz: number }
type FooReturn = ReturnType<typeof foo>; // { baz: number }

実際には、事実上すべてのパブリックメンバーを取得できます:Ivan Koshelevのこのブログ投稿を参照してください

function foo() {
return {
a: 1,
b: 2,
subInstArr: [
{
c: 3,
d: 4,
},
],
};
}

type InstType = ReturnType<typeof foo>;
type SubInstArr = InstType["subInstArr"];
type SubInstType = SubInstArr[0];

let baz: SubInstType = {
c: 5,
d: 6, // type checks ok!
};

//You could just write a one-liner,
//But please make sure it is forward-readable
//(you can understand it from reading once left-to-right with no jumps)
type SubInstType2 = ReturnType<typeof foo>["subInstArr"][0];
let baz2: SubInstType2 = {
c: 5,
d: 6, // type checks ok!
};
  • TSには、関数の引数を抽出するためのParametersユーティリティ型も付属しています
  • より「カスタム」なものについては、inferキーワードがこれの基本的な構成要素ですが、慣れるまでに少し時間がかかります。 上記のユーティリティ型のソースコードとこの例を見て、アイデアを得てください。 Basaratは、inferに関する良いビデオも持っています。

必要な型が存在しません!

エクスポートされていない型を持つモジュールよりも厄介なことは何ですか? 型指定されていないモジュールです!

続行する前に、DefinitelyTypedまたはTypeSearchに型が存在しないことを確認してください

心配しないでください! この問題を解決するには、いくつかの方法があります。

あらゆるものに`any`を適用する

より**手抜きな**方法は、新しい型宣言ファイル(例:`typedec.d.ts`)を作成することです。まだ作成していない場合、ディレクトリのルートにある`tsconfig.json`ファイルの`include`配列を確認して、TypeScriptがファイルのパスを解決できるようにしてください。

// inside tsconfig.json
{
// ...
"include": [
"src" // automatically resolves if the path to declaration is src/typedec.d.ts
]
// ...
}

このファイル内で、目的のモジュール(例:`my-untyped-module`)の`declare`構文を宣言ファイルに追加します。

// inside typedec.d.ts
declare module "my-untyped-module";

エラーなしで動作させるだけであれば、この1行だけで十分です。さらにハック的で、一度書いて忘れられる方法は、代わりに`"*"`を使用することです。これにより、既存および将来のすべての型指定されていないモジュールに`Any`型が適用されます。

この解決策は、型指定されていないモジュールが数個未満の場合の回避策として有効です。それ以上になると、時間差爆弾を抱えていることになります。この問題を回避する唯一の方法は、以下のセクションで説明するように、型指定されていないモジュールの不足している型を定義することです。

型を自動生成する

TypeScriptに`--allowJs`と`--declaration`を付けて使用すると、ライブラリの型についてTypeScriptが「最良の推測」を行う様子を確認できます。

これがうまくいかない場合は、`dts-gen`を使用して、オブジェクトのランタイム形状を使用して、使用可能なすべてのプロパティを正確に列挙します。これは非常に正確な傾向がありますが、このツールはまだJSDocコメントをスクレイピングして追加の型を生成することをサポートしていません。

npm install -g dts-gen
dts-gen -m <your-module>

JSからTSへの変換ツールや移行戦略は他にも自動化されたものがあります。 移行チートシートをご覧ください。

エクスポートされたフックの型指定

フックの型指定は、純粋関数の型指定と同じです。

以下の手順は、2つの前提条件に基づいています。

  • このセクションの前半で述べたように、型宣言ファイルが既に作成されていること。
  • ソースコード、特に使用する関数を直接エクスポートするコードにアクセスできること。ほとんどの場合、`index.js`ファイルに格納されています。通常、フックを完全に定義するには、少なくとも**2つ**の型宣言(**入力プロップ**用と**戻りプロップ**用)が必要です。型指定するフックが以下の構造に従っているとします。
// ...
const useUntypedHook = (prop) => {
// some processing happens here
return {
/* ReturnProps */
};
};
export default useUntypedHook;

その場合、型宣言は、おそらく以下の構文に従う必要があります。

declare module 'use-untyped-hook' {
export interface InputProps { ... } // type declaration for prop
export interface ReturnProps { ... } // type declaration for return props
export default function useUntypedHook(
prop: InputProps
// ...
): ReturnProps;
}
たとえば、useDarkModeフックは、同様の構造に従う関数をエクスポートします。
// inside src/index.js
const useDarkMode = (
initialValue = false, // -> input props / config props to be exported
{
// -> input props / config props to be exported
element,
classNameDark,
classNameLight,
onChange,
storageKey = "darkMode",
storageProvider,
global,
} = {}
) => {
// ...
return {
// -> return props to be exported
value: state,
enable: useCallback(() => setState(true), [setState]),
disable: useCallback(() => setState(false), [setState]),
toggle: useCallback(() => setState((current) => !current), [setState]),
};
};
export default useDarkMode;

コメントが示唆するように、前述の構造に従ってこれらの設定プロップと戻りプロップをエクスポートすると、以下の型のエクスポートが生成されます。

declare module "use-dark-mode" {
/**
* A config object allowing you to specify certain aspects of `useDarkMode`
*/
export interface DarkModeConfig {
classNameDark?: string; // A className to set "dark mode". Default = "dark-mode".
classNameLight?: string; // A className to set "light mode". Default = "light-mode".
element?: HTMLElement; // The element to apply the className. Default = `document.body`
onChange?: (val?: boolean) => void; // Override the default className handler with a custom callback.
storageKey?: string; // Specify the `localStorage` key. Default = "darkMode". Set to `null` to disable persistent storage.
storageProvider?: WindowLocalStorage; // A storage provider. Default = `localStorage`.
global?: Window; // The global object. Default = `window`.
}
/**
* An object returned from a call to `useDarkMode`.
*/
export interface DarkMode {
readonly value: boolean;
enable: () => void;
disable: () => void;
toggle: () => void;
}
/**
* A custom React Hook to help you implement a "dark mode" component for your application.
*/
export default function useDarkMode(
initialState?: boolean,
config?: DarkModeConfig
): DarkMode;
}

エクスポートされたコンポーネントの型指定

型指定されていないクラスコンポーネントの型指定の場合、型を宣言した後、`class UntypedClassComponent extends React.Component<UntypedClassComponentProps, any> {}`を使用して型を拡張してエクスポートするという点を除けば、アプローチにほとんど違いはありません。ここで、`UntypedClassComponentProps`は型宣言を保持します。

たとえば、sw-yxのReact Router 6の型に関するGistでは、当時型指定されていなかったRR6の型指定に同様の方法を実装していました。

declare module "react-router-dom" {
import * as React from 'react';
// ...
type NavigateProps<T> = {
to: string | number,
replace?: boolean,
state?: T
}
//...
export class Navigate<T = any> extends React.Component<NavigateProps<T>>{}
// ...

クラスコンポーネントの型定義の作成の詳細については、参考としてこの投稿を参照してください。

TypeScriptでよくある既知の問題

React開発者が頻繁に遭遇する問題のリストです。TSには解決策がありません。必ずしもTSXだけではありません。

TypeScriptは、オブジェクト要素のnullチェック後も絞り込みを行いません

https://pbs.twimg.com/media/E0u6b9uUUAAgwAk?format=jpg&name=medium

参照:https://mobile.twitter.com/tannerlinsley/status/1390409931627499523https://github.com/microsoft/TypeScript/issues/9998も参照してください。

TypeScriptでは、子要素の型を制限できません

この種のAPIの型安全性を保証することはできません。

<Menu>
<MenuItem/> {/* ok */}
<MenuLink/> {/* ok */}
<div> {/* error */}
</Menu>

出典:https://twitter.com/ryanflorence/status/1085745787982700544?s=20