TypeScript实用程序可将多个相交的类型合并为单个“基本”类型

时间:2019-06-05 04:32:33

标签: reactjs typescript typescript2.0

当我有一个类型属性,要求该类型中的另一个属性为特定值时,我的类型会变得过于复杂。我想写一个抽象逻辑的实用程序类型。

示例。假设我写了一个与正在构建的Text(React)组件相关的类型:

type Props = {
  children: React.ReactNode; // Just a React type, not pertinent to the question
  as: "p" | "span" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; // The html tag for the text
  truncate?: boolean; // Whether to truncate the text when it is long
  expanded?: boolean; // The truncated text is expanded when this is true
  bigHeading?: boolean; // When `as` is `"h1" | "h2" | "h3" | "h4" | "h5" | "h6"`, this property makes a normal heading extra large.
}
除非expanded?truncate?,否则

true不会执行任何操作,除非bigHeading?as,否则"h1" | "h2" | "h3" | "h4" | "h5" | "h6"不会执行任何操作。因此,除非满足这些条件,否则我希望Props类型禁止expandedbigHeading

我的解决方法是:

type BaseProps = {
  children: React.ReactNode;
};

type NoTruncateProps = BaseProps & {
  as: "p" | "span";
  truncate?: false;
};

type TruncateProps = BaseProps & {
  as: "p" | "span";
  truncate: true;
  expanded?: boolean;
};

type NoTruncateHeadingProps = BaseProps & {
  as: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
  bigHeading?: boolean;
  truncate?: false;
};

type TruncateHeadingProps = BaseProps & {
  as: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
  bigHeading?: boolean;
  truncate: true;
  expanded?: boolean;

};

type Props = NoTruncateProps | TruncateProps | NoTruncateHeadingProps | TruncateHeadingProps

这显然太复杂了。我将如何简化和抽象这种逻辑以实现可重用性?

相关:TypeScript utility type for conditional props (based on entered value of other properties in the type)

1 个答案:

答案 0 :(得分:2)

这种类型的逻辑非常复杂,因此无论哪种方式,该类型都将非常复杂。我将使用的方法是将每个逻辑分解为单独的类型,然后将它们相交。

对于truncateexpanded,我们将得到:

  type Truncatable = {
    truncate: true;
    expanded?: boolean
  } | {
    truncate?: false;
  }

对于asbigHeading,我们将得到:

type HeadingOrText = {
    as: "p" | "span"
} | {
    as: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
    bigHeading?: boolean;
}

然后我们可以将它们相交以获得所需的最终结合。尽管here概述了过多的属性检查,但有一个复杂之处,我们将使用此处提供的解决方案。我们还可以使用Id类型展平道具,以获得更好的错误和工具提示:

type UnionKeys<T> = T extends any ? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends any ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>

type Id<T> = {} & { [P in keyof T]: T[P] }

type Truncateable = {
    truncate: true;
    expanded?: boolean
} | {
    truncate?: false;
}
type HeadingOrText = {
    as: "p" | "span"
} | {
    as: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
    bigHeading?: boolean;
}

type WithChildren = {
    children: React.ReactNode;
}
type Props = Id<StrictUnion<HeadingOrText & WithChildren & Truncateable>>

let h1: Props = {
    as: "h1",
    bigHeading: true,
    truncate: true,
    expanded: false,
    children: []
}


let hErr: Props = {
    as: "h1",
    bigHeading: true,
    truncate: false,
    expanded: false, // causes error
    children: []
}



let p: Props = { // ok
    as: "p",
    children: []
}

let pErr: Props = {
    as: "p",
    children: [],
    bigHeading: true // causes error
}

虽然不一定需要更少的代码,但我认为此解决方案可以很好地组合在一起,并允许在联合中添加其他类似mixin的类型。