网站搜索

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 <></>;
};

操场