如何在 TypeScript 中像专业人士一样为 React props 设置类型
第一部分
第二部分
第三部分
第四部分
本文由四个互不相关的部分组成。
所有使用 TypeScript 和 React 的人都知道如何给 props 添加类型定义,对吧?
第一部分
假设我们有三个有效状态A,、B和C。
enum Mode {
easy = 'easy',
medium = 'medium',
hard = 'hard'
}
type A = {
mode: Mode.easy;
data: string;
check: (a: A['data']) => string
}
type B = {
mode: Mode.medium;
data: number;
check: (a: B['data']) => number
}
type C = {
mode: Mode.hard;
data: number[];
check: (a: C['data']) => number
}
现在,我们必须确保我们的组件只接受有效的 props:
type Props = A | B | C;
const Comp: FC<Props> = (props) => {
if (props.mode === Mode.easy) {
const x = props // A
}
if (props.mode === Mode.medium) {
const x = props // B
}
if (props.mode === Mode.hard) {
const x = props // C
}
return null
}
很简单,对吧?
现在,试试props.check在条件语句之外调用它。
const Comp: FC<Props> = (props) => {
props.check(props.data) // error
return null
}
但为什么会出错?
TL;DR;
在反向变体位置上存在多个相同类型变量的候选者,会导致推断出交叉类型。
就我们而言:
type Intersection = string & number & number[] // never
这就是为什么check需要never类型。
差点忘了,请不要忘记解构,它与 TypeScript 类型推断不太兼容:
const Comp: FC<Props> = ({ check, data, mode }) => {
if (mode === Mode.easy) {
check(data) // error
}
return null
}
如果要使用解构,请同时使用类型保护符。
const isEasy = <M extends Mode>(
mode: M,
check: Fn
): check is Extract<Props, { mode: Mode.easy }>['check'] =>
mode === Mode.easy
既然我们在代码库中添加了额外的功能,我们就应该进行测试,对吧?
我想直接告诉你方法,无需任何额外检查。
我并不是说这种方法比使用类型保护更安全或更好。事实上,它并非如此。如果您不想对应用程序的业务逻辑进行任何更改,可以使用这种方法。更改之后,没人会要求您编写单元测试 :) 想象一下,您只需要从 [此处应填写具体类型] 迁移js到 [此处应填写具体类型] 的情况ts。
为了实现调用,check我们需要对其进行重载。
让我们把练习分成 5 个小任务:
1. 获取属性为函数的键名。
// Get keys where value is a function
type FnProps<T> = {
[Prop in keyof T]: T[Prop] extends Fn ? Prop : never
}[keyof T]
// check
type Result0 = FnProps<Props>
2. 获取所有函数的并集。
type Values<T> = T[keyof T]
type FnUnion<PropsUnion> = Values<Pick<PropsUnion, FnProps<PropsUnion>>>
// | ((a: A['data']) => string)
// | ((a: B['data']) => number)
// | ((a: C['data']) => number)
type Result1 = FnUnion<Props>
3. 计算不太具体的过载
type ParametersUnion<PropsUnion> =
FnUnion<PropsUnion> extends Fn
? (a: Parameters<FnUnion<PropsUnion>>[0]) =>
ReturnType<FnUnion<PropsUnion>>
: never
// (a: string | number | number[]) => string | number
type Result2 = ParametersUnion<Props>
4. 为了将函数并集转换为重载,我们需要使用交集而不是并集。因此,让我们将函数并集与一个不太具体的重载合并。
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
type Overload<PropsUnion> =
& UnionToIntersection<PropsUnion[FnProps<PropsUnion>]>
& ParametersUnion<PropsUnion>
// & ((a: A['data']) => string)
// & ((a: B['data']) => number)
// & ((a: C['data']) => number)
// & ((a: string | number | number[]) => string | number)
type Result3 = Overload<Props>
5. 最后一步。我们需要将合并后的函数与重载函数合并。换句话说,我们将重写 checkproperty 函数。
type OverloadedProps<PropsUnion> =
& PropsUnion
& Record<FnProps<PropsUnion>, Overload<PropsUnion>>
// Props & Record<"check", Overload<Props>>
type Result4 = OverloadedProps<Props>
完整示例:
import React, { FC } from 'react'
enum Mode {
easy = 'easy',
medium = 'medium',
hard = 'hard'
}
type A = {
mode: Mode.easy;
data: string;
check: (a: A['data']) => string
}
type B = {
mode: Mode.medium;
data: number;
check: (a: B['data']) => number
}
type C = {
mode: Mode.hard;
data: number[];
check: (a: C['data']) => number
}
type Fn = (...args: any[]) => any
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
type Props = A | B | C;
// Get keys where value is a function
type FnProps<T> = {
[Prop in keyof T]: T[Prop] extends Fn ? Prop : never
}[keyof T]
// check
type Result0 = FnProps<Props>
type Values<T> = T[keyof T]
type FnUnion<PropsUnion> = Values<Pick<PropsUnion, FnProps<PropsUnion>>>
// | ((a: A['data']) => string)
// | ((a: B['data']) => number)
// | ((a: C['data']) => number)
type Result1 = FnUnion<Props>
type ParametersUnion<PropsUnion> =
FnUnion<PropsUnion> extends Fn
? (a: Parameters<FnUnion<PropsUnion>>[0]) =>
ReturnType<FnUnion<PropsUnion>>
: never
// (a: string | number | number[]) => string | number
type Result2 = ParametersUnion<Props>
type Overload<PropsUnion> =
& UnionToIntersection<PropsUnion[FnProps<PropsUnion>]>
& ParametersUnion<PropsUnion>
// & ((a: A['data']) => string)
// & ((a: B['data']) => number)
// & ((a: C['data']) => number)
// & ((a: string | number | number[]) => string | number)
type Result3 = Overload<Props>
type OverloadedProps<PropsUnion> =
& PropsUnion
& Record<FnProps<PropsUnion>, Overload<PropsUnion>>
// Props & Record<"check", Overload<Props>>
type Result4 = OverloadedProps<Props>
const Comp: FC<OverloadedProps<Props>> = (props) => {
const { mode, data, check } = props;
if (props.mode === Mode.easy) {
props.data // string
}
const result = check(data) // string | number
return null
}
请注意,这种输入 props 的方式是错误的。请将其视为临时解决方案。
第二部分
让我们来看 Stack Overflow 上的另一个例子。
我有两个组件,它们的 props 很相似,但有一个关键区别。其中一个组件TabsWithState只接受一个 prop tabs,它是一个对象数组,数组的元素形状如下:
interface ItemWithState {
name: string
active: boolean;
}
interface WithStateProps {
tabs: ItemWithState[];
};
另一个类似的……
我们有两个具有相似属性的组件,其中tabs一个属性是公共的:
interface ItemWithState {
name: string;
active: boolean;
}
interface ItemWithRouter {
name: string;
path: string;
}
type WithStateProps = {
tabs: ItemWithState[];
};
type WithRouterProps = {
withRouter: true;
baseUrl?: string;
tabs: ItemWithRouter[];
};
const TabsWithRouter: FC<WithRouterProps> = (props) => null
const TabsWithState: FC<WithStateProps> = (props) => null
此外,我们还有高阶分量:
type TabsProps = WithStateProps | WithRouterProps;
const Tabs = (props: TabsProps) => {
if (props.withRouter) { // error
return <TabsWithRouter {...props} />; // error
}
return <TabsWithState {...props} />; // error
};
我们最终出现了三个失误。
由于该属性是可选的, TypeScript 不允许您获取withRouter它。它只允许您获取公共属性tabs。这是预期行为。
有一个修复/变通方法。我们可以添加withRouter?:never到WithStateProps类型定义中。
现在它能正常工作并推断出类型{...props}。但它有一个小缺点:它允许我们向Tabs组件传递非法属性:
import React, { FC } from 'react'
interface ItemWithState {
name: string;
active: boolean;
}
interface ItemWithRouter {
name: string;
path: string;
}
type WithStateProps = {
withRouter?: never;
tabs: ItemWithState[];
};
type WithRouterProps = {
withRouter: true;
baseUrl?: string;
tabs: ItemWithRouter[];
};
const TabsWithRouter: FC<WithRouterProps> = (props) => null
const TabsWithState: FC<WithStateProps> = (props) => null
type TabsProps = WithStateProps | WithRouterProps;
const Tabs = (props: TabsProps) => {
if (props.withRouter) {
return <TabsWithRouter {...props} />;
}
return <TabsWithState {...props} />;
};
const Test = () => {
return (
<div>
<Tabs // With incorrect state props
baseUrl="something"
tabs={[{ name: "myname", active: true }]}
/>
</div>
);
};
这种方法不好。我们试试用 typeguard:
interface ItemWithState {
name: string;
active: boolean;
}
interface ItemWithRouter {
name: string;
path: string;
}
type WithStateProps = {
tabs: ItemWithState[];
};
type WithRouterProps = {
withRouter: true;
baseUrl?: string;
tabs: ItemWithRouter[];
};
const TabsWithRouter: FC<WithRouterProps> = (props) => null
const TabsWithState: FC<WithStateProps> = (props) => null
type TabsProps = WithStateProps | WithRouterProps;
const hasProperty = <Obj, Prop extends string>(obj: Obj, prop: Prop)
: obj is Obj & Record<Prop, unknown> =>
Object.prototype.hasOwnProperty.call(obj, prop);
const Tabs = (props: TabsProps) => {
if (hasProperty(props, 'withRouter')) {
return <TabsWithRouter {...props} />;
}
return <TabsWithState {...props} />;
};
const Test = () => {
return (
<div>
<Tabs // With incorrect state props
baseUrl="something"
tabs={[{ name: "myname", active: true }]}
/>
</div>
);
};
我认为这种方法要好得多,因为我们不需要使用任何额外的可选属性hacks。我们的WithStateProps类型不应该有任何额外的可选属性。但它仍然存在同样的缺点:允许存在非法状态。
我们似乎忘记了函数重载。由于 React 组件本质上也是简单的函数,所以它的运作方式也相同。
请记住,函数的交集会产生重载:
// type Overload = FC<WithStateProps> & FC<WithRouterProps>
const Tabs: FC<WithStateProps> & FC<WithRouterProps> = (props: TabsProps) => {
if (hasProperty(props, 'withRouter')) {
return <TabsWithRouter {...props} />;
}
return <TabsWithState {...props} />;
};
const Test = () => {
return (
<div>
<Tabs // With correct state props
tabs={[{ name: "myname", active: true }]}
/>
<Tabs // With incorrect state props
baseUrl="something"
tabs={[{ name: "myname", active: true }]}
/>
<Tabs // WIth correct router props
withRouter
tabs={[{ name: "myname", path: "somepath" }]}
/>
<Tabs // WIth correct router props
withRouter
baseUrl="someurl"
tabs={[{ name: "myname", path: "somepath" }]}
/>
<Tabs // WIth incorrect router props
withRouter
tabs={[{ name: "myname", active: true }]}
/>
</div>
);
};
问:如果并集中有 5 个元素怎么办?
答:我们可以使用条件分布类型:
import React, { FC } from 'react'
interface ItemWithState {
name: string;
active: boolean;
}
interface ItemWithRouter {
name: string;
path: string;
}
type WithStateProps = {
tabs: ItemWithState[];
};
type WithRouterProps = {
withRouter: true;
baseUrl?: string;
tabs: ItemWithRouter[];
};
const TabsWithRouter: FC<WithRouterProps> = (props) => null
const TabsWithState: FC<WithStateProps> = (props) => null
type TabsProps = WithStateProps | WithRouterProps;
const hasProperty = <Obj, Prop extends string>(obj: Obj, prop: Prop)
: obj is Obj & Record<Prop, unknown> =>
Object.prototype.hasOwnProperty.call(obj, prop);
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
type Distributive<T> = T extends any ? FC<T> : never
type Overload = UnionToIntersection<Distributive<TabsProps>>
const Tabs: Overload = (props: TabsProps) => {
if (hasProperty(props, 'withRouter')) {
return <TabsWithRouter {...props} />;
}
return <TabsWithState {...props} />;
};
const Test = () => {
return (
<div>
<Tabs // With correct state props
tabs={[{ name: "myname", active: true }]}
/>
<Tabs // With incorrect state props
baseUrl="something"
tabs={[{ name: "myname", active: true }]}
/>
<Tabs // WIth correct router props
withRouter
tabs={[{ name: "myname", path: "somepath" }]}
/>
<Tabs // WIth correct router props
withRouter
baseUrl="someurl"
tabs={[{ name: "myname", path: "somepath" }]}
/>
<Tabs // WIth incorrect router props
withRouter
tabs={[{ name: "myname", active: true }]}
/>
</div>
);
};
您也可以采用这种方法:
type Overloading =
& ((props: WithStateProps) => JSX.Element)
& ((props: WithRouterProps) => JSX.Element)
这是风格问题。
希望你还没累。
第三部分
假设我们有Animal一个组件,它具有以下约束条件:
- 如果
dogName为空字符串或未设置,canBark则应为 false - 如果
dogName字符串不为空,canBark则应为真。
type NonEmptyString<T extends string> = T extends '' ? never : T;
type WithName = {
dogName: string,
canBark: true,
}
type WithoutName = {
dogName?: '',
canBark: false
};
type Props = WithName | WithoutName;
由于 React 组件只是一个普通的函数,我们可以对其进行重载,甚至可以使用一些通用参数:
type Overloadings =
& ((arg: { canBark: false }) => JSX.Element)
& ((arg: { dogName: '', canBark: false }) => JSX.Element)
& (<S extends string>(arg: { dogName: NonEmptyString<S>, canBark: true }) => JSX.Element)
const Animal: Overloadings = (props: Props) => {
return null as any
}
我们来测试一下:
import React, { FC } from 'react'
type NonEmptyString<T extends string> = T extends '' ? never : T;
type WithName = {
dogName: string,
canBark: true,
}
type WithoutName = {
dogName?: '',
canBark: false
};
type Props = WithName | WithoutName;
type Overloadings =
& ((arg: { canBark: false }) => JSX.Element)
& ((arg: { dogName: '', canBark: false }) => JSX.Element)
& (<S extends string>(arg: { dogName: NonEmptyString<S>, canBark: true }) => JSX.Element)
const Animal: Overloadings = (props: Props) => {
return null as any
}
const Test = () => {
return (
<>
<Animal dogName='' canBark={false} /> // ok
<Animal dogName='a' canBark={true} /> // ok
<Animal canBark={false} /> // ok
<Animal dogName='a' canBark={false} /> // error
<Animal dogName='' canBark={true} /> // error
<Animal canBark={true} /> // error
</>
)
}
第四部分
假设我们有一个组件,它期望foo属性bar是字符串,但属性foo不能是空字符串hello。
为了实现这一点,我们应该使用显式的泛型 forfoo和bar属性。
这很简单:
import React from 'react'
type Props<F extends string = '', B extends string = ''> = {
foo: F;
bar: B;
}
type ConditionalProps<T> = T extends { foo: infer Foo; bar: string } ? Foo extends 'hello' ? never : T : never
const Example = <F extends string, B extends string>(props: ConditionalProps<Props<F, B>>) => {
return null as any
}
const Test = () => {
<>
<Example foo='hello' bar='bye' /> // expected error
<Example foo='not hello' bar='1' /> // ok
</>
}
感谢阅读。
文章来源:https://dev.to/captainyossarian/how-to-type-react-props-as-a-pro-2df2