defaultProps の型付け
defaultProps
は必要ないかもしれません
このツイート にあるように、defaultProps は最終的に非推奨になるでしょう。議論についてはこちらを確認してください
共通認識としては、オブジェクトのデフォルト値を使用することです。
関数コンポーネント
type GreetProps = { age?: number };
const Greet = ({ age = 21 }: GreetProps) => // etc
クラスコンポーネント
type GreetProps = {
age?: number;
};
class Greet extends React.Component<GreetProps> {
render() {
const { age = 21 } = this.props;
/*...*/
}
}
let el = <Greet age={3} />;
defaultProps
の型付け
defaultProps
の型推論は TypeScript 3.0+ で大幅に改善されましたが、いくつかのエッジケースは依然として問題があります。
関数コンポーネント
// using typeof as a shortcut; note that it hoists!
// you can also declare the type of DefaultProps if you choose
// e.g. https://github.com/typescript-cheatsheets/react/issues/415#issuecomment-841223219
type GreetProps = { age: number } & typeof defaultProps;
const defaultProps = {
age: 21,
};
const Greet = (props: GreetProps) => {
// etc
};
Greet.defaultProps = defaultProps;
クラスコンポーネントの場合、いくつかの方法があります (Pick
ユーティリティ型を使用するなど) が、推奨されるのは props 定義を「逆にする」ことです
type GreetProps = typeof Greet.defaultProps & {
age: number;
};
class Greet extends React.Component<GreetProps> {
static defaultProps = {
age: 21,
};
/*...*/
}
// Type-checks! No type assertions needed!
let el = <Greet age={3} />;
ライブラリ作成者向けの React.JSX.LibraryManagedAttributes
のニュアンス
上記の実装はアプリ作成者にとっては問題ありませんが、GreetProps
をエクスポートして他の人が利用できるようにしたい場合があります。ここで問題となるのは、GreetProps
の定義方法では、defaultProps
のために必須ではないにもかかわらず、age
が必須の prop になっていることです。
ここで理解すべき点は、GreetProps
はコンポーネントの 内部 契約であり、外部 のコンシューマー向けの契約ではないということです。エクスポート専用の型を別途作成するか、React.JSX.LibraryManagedAttributes
ユーティリティを利用することができます
// internal contract, should not be exported out
type GreetProps = {
age: number;
};
class Greet extends Component<GreetProps> {
static defaultProps = { age: 21 };
}
// external contract
export type ApparentGreetProps = React.JSX.LibraryManagedAttributes<
typeof Greet,
GreetProps
>;
これは正しく機能しますが、ApparentGreetProps
にカーソルを合わせると少し戸惑うかもしれません。このボイラープレートは、後述する ComponentProps
ユーティリティで削減できます。
defaultProps を持つコンポーネントの Props の利用
defaultProps
を持つコンポーネントは、実際には必須ではないいくつかの必須 prop を持っているように見えるかもしれません。
問題提起
これはあなたがやりたいことです
interface IProps {
name: string;
}
const defaultProps = {
age: 25,
};
const GreetComponent = ({ name, age }: IProps & typeof defaultProps) => (
<div>{`Hello, my name is ${name}, ${age}`}</div>
);
GreetComponent.defaultProps = defaultProps;
const TestComponent = (props: React.ComponentProps<typeof GreetComponent>) => {
return <h1 />;
};
// Property 'age' is missing in type '{ name: string; }' but required in type '{ age: number; }'
const el = <TestComponent name="foo" />;
解決策
React.JSX.LibraryManagedAttributes
を適用するユーティリティを定義します
type ComponentProps<T> = T extends
| React.ComponentType<infer P>
| React.Component<infer P>
? React.JSX.LibraryManagedAttributes<T, P>
: never;
const TestComponent = (props: ComponentProps<typeof GreetComponent>) => {
return <h1 />;
};
// No error
const el = <TestComponent name="foo" />;
その他の議論と知識
なぜ React.FC
は defaultProps
を壊すのですか?
TypeScript 2.9 以前
TypeScript 2.9 以前では、複数の方法がありますが、これはこれまでで最も良いアドバイスです
type Props = Required<typeof MyComponent.defaultProps> & {
/* additional props here */
};
export class MyComponent extends React.Component<Props> {
static defaultProps = {
foo: "foo",
};
}
以前の推奨事項では、TypeScript の Partial type
機能を使用していました。これは、現在のインターフェースがラップされたインターフェースの部分的なバージョンを満たすことを意味します。これにより、型を変更することなく defaultProps を拡張できます。
interface IMyComponentProps {
firstProp?: string;
secondProp: IPerson[];
}
export class MyComponent extends React.Component<IMyComponentProps> {
public static defaultProps: Partial<IMyComponentProps> = {
firstProp: "default",
};
}
このアプローチの問題点は、React.JSX.LibraryManagedAttributes
で動作する型推論で複雑な問題が発生することです。基本的に、このコンポーネントで JSX 式を作成すると、その props がすべてオプションであるとコンパイラが考えるようになります。
@ferdaber によるコメントはこちら と こちら を参照してください。