reactjs - 基于同一类型内部属性的类型
我正在尝试制作一个组件,该组件允许基于类型内另一个属性的属性。
我试图实现的东西有点像判别联合类型,但不同的是,我希望判别器是一个字面量数组,并且能够根据属性中的内容返回多个类型的交集。
因此,给出以下定义:
type Variant = 'select' | 'drag'
// Common interface that defines props available no matter what functionality is required
interface CommonProps {
variant: Variant[]
title: string
}
// Drag interface specifying props only available when drag functionality is required
interface DragProps extends CommonProps {
onDrag: () => void
}
// Select interface specifying props only available when select functionality is required
interface SelectProps extends CommonProps {
onSelect: () => void
}
const ComponentWithRequiredFeatures = (props: MagicTypeProps) => {
...
}
我正在尝试制作一些 MagicTypeProps,以便我可以像这样使用这个组件:
// Error: Property 'onDrag' does not exist on type
<ComponentWithRequiredFeatures variant={['select']} title="Hello world" onSelect={() => {}} onDrag={() => {}} />
// Error: Property 'onSelect' is missing
<ComponentWithRequiredFeatures variant={['select', 'drag']} title="Hello world" onDrag={() => {}} />
// Okay
<ComponentWithRequiredFeatures variant={['select', 'drag']} title="Hello world" onSelect={() => {}} onDrag={() => {}} />
所以基于 variant 属性,我期望 MagicTypeProps 是原始类型的某种交集版本。
例如:
如果 variant 是 ['select'],那么 MagicTypeProps 将是 SelectProps
如果 variant 是 ['select', 'drag'],那么它将是 SelectProps & DragProps。
到目前为止我尝试过的
我已经通过使用泛型类型和字面量实现了相同的行为,只是从上面的 CommonProps 类型中移除了 variant 属性。
使用通用类型
// https://github.com/microsoft/TypeScript/issues/14829#issuecomment-980401795
export type NoInfer<T> = T extends infer S ? S : never
type GenericExtend = CommonProps | DragProps | SelectProps
const ComponentWithRequiredFeatures = <T extends GenericExtend = CommonProps>(props: NoInfer<T>) => {
...
}
这实现了上述描述的行为并产生所需的错误:
// Error: Property 'onDrag' does not exist on type 'IntrinsicAttributes & SelectProps'.
<ComponentWithRequiredFeatures<SelectProps> title="Hello world" onSelect={() => {}} onDrag={() => {}} />
// Error: Property 'onSelect' is missing in type '{ title: string; onDrag: () => void; }' but required in type 'SelectProps'.
<ComponentWithRequiredFeatures<SelectProps & DragProps> title="Hello world" onDrag={() => {}} />
// Okay
<ComponentWithRequiredFeatures<SelectProps & DragProps> title="Hello world" onDrag={() => {}} onSelect={() => {}} />
使用带字面量的泛型类型
// https://github.com/microsoft/TypeScript/issues/29594
export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never
type LiteralPropsMapping = {
common: CommonProps
select: SelectProps
drag: DragProps
}
type Literals = keyof LiteralPropsMapping // 'common' | 'drag' | 'select'
const ComponentWithRequiredFeatures = <T extends Literals = 'common'>(
props: UnionToIntersection<LiteralPropsMapping [T]>
) => {
...
}
这再次在使用时达到相同的效果:
// Error: Property 'onDrag' does not exist on type 'IntrinsicAttributes & SelectProps'.
<ComponentWithRequiredFeatures<'select'> title="Hello world" onSelect={() => {}} onDrag={() => {}} />
// Property 'onSelect' is missing in type '{ title: string; onDrag: () => void; }' but required in type 'SelectProps'.
<ComponentWithRequiredFeatures<'select' | 'drag'> title="Hello world" onDrag={() => {}} />
// Okay
<ComponentWithRequiredFeatures<'select' | 'drag'> title="Hello world" onDrag={() => {}} onSelect={() => {}} />
Codesandbox 链接
我创建了一个 Codesandbox 来展示我所有的尝试,以及类型检查在泛型方法中的工作方式,我只是在不知道当约束需要从类型中的一个属性推断时,如何实现这样的事情。
答案#1
让我们从将变体映射到它们各自的属性类型开始:
interface VariantMap {
select: SelectProps;
drag: DragProps;
}
你的 Variant 类型可以表示为
type Variant = keyof VariantMap;
为了获取联合变体的正确属性类型,我们交集共享的 CommonProps,
interface CommonProps<Variants extends Variant> {
variant?: Variants[];
title: string;
}
使用来自 VariantMap 的每个相应条目。使用分配条件类型,它给出了我们以下结果:
type PropsForVariants<Variants extends Variant> = CommonProps<Variants> &
([Variants] extends [never] // check if it's never (not provided)
? {}
: Variants extends Variant // distributes Variants
? VariantMap[Variants]
: {});
然后,使你的组件相对于给定的变体成为通用组件(如果没有提供变体,则默认为 never),我们可以将变体传递给类型:
const ComponentWithRequiredFeatures = <Variants extends Variant = never>(
props: PropsForVariants<Variants>
) => {
return <></>;
};
操场
