Typescript严格对象键名,但允许键名的选项

hlswsv35  于 2023-04-22  发布在  TypeScript
关注(0)|答案(2)|浏览(150)

我试图为一个主题创建一个严格的Map,但我希望Map的键不是动态的,而是有可选的键值。|AppBackground”可以有其中一个键和相同的值。下面是我尝试过的一些事情。我总是希望允许像red:color这样的严格键值对。

type 300 = '300' | 'CardBackground';
export type ThemeMapping = {
  [key: '100' | 'AppBackground']: Color;
  [key in '200' | 'ContentBackground']: Color;
  [key: 300 ]: Color;
  ['400' | 'InactiveBackground']: Color;
  link: Color;
  red: Color;
  yellow: Color;
  green: Color;
  blue: Color;
}

我知道对于动态值,你也可以使用{[key: string]: Color}。我只是希望键类型本质上是选项,而不是动态的,或者只是一个普通的字符串。
请让我知道如果你需要我解释更多。

42fyovps

42fyovps1#

可以合并多个类型来实现此行为。

type MyFirstType = "100" | "AppBackground"

type MySecondType = "200" | "ContentBackground"

type CombinedType = MyFirstType | MySecondType | "red" | "link"

type ThemeMapping = Record<CombinedType, Color>

这就是你想要的吗

fhity93d

fhity93d2#

TypeScript本身并不支持“一个对象只具有某个集合中的一个属性”的概念,所以如果你想模拟它,你需要自己编写一些以这种方式工作的东西。这里有一种方法:

type ExactlyOneProp<T> = {
    [K in keyof T]-?: Pick<T, K> & { [P in Exclude<keyof T, K>]?: never }
}[keyof T];

这个想法是ExactlyOneProp<T>应该导致一个联合类型,其中T的每个属性都有一个成员,其中每个联合成员应该要求一个键并禁止其他成员。相反,您可以创建一个可选属性,其值是不可能的never类型,因此唯一可接受的做法是忽略该属性(或者根据编译器选项将其设置为undefined)。因此ExactlyOneProp<{a: 0, b: 1, c: 2}>应该等效于{a: 0, b?: never, c?: never} | {a?: never, b: 1, c?: never} | {a?: never, b?: never, c: 2}
ExactlyOneProp<T>通过mappingT中的每个属性密钥K上工作,并创建一个Pick<T, K> & {[P in Exclude<keyof T, K>]?: never}类型的新属性(使用Pick<T, K>实用程序类型作为仅具有已知K属性的对象),然后将其与另一个Map类型(具有Exclude<keyof T, K>中的键)交叉(使用Exclude<X, Y>实用程序类型从T的键中筛选出K),所有可选属性(来自?Map修饰符)的类型为never
这会生成一个相当难看的类型,因此我们将通过

type Pretty<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;

其实质上只是将所有对象交叉折叠成它们的等效单对象类型(例如,{a: 0} & {b: 1}变为{a: 0, b: 1})。
让我们测试一下:

type Test = Pretty<ExactlyOneProp<{a: 0, b: 1, c: 2}>>;
/* type Test = 
   { a: 0; b?: undefined; c?: undefined; } | 
   { b: 1; a?: undefined; c?: undefined; } | 
   { c: 2; a?: undefined; b?: undefined; };
*/

看起来不错
所以现在要按照你想要的方式生成ThemeMapping,我们需要将一堆不同的片段相交,像这样:

type ThemeMapping = Pretty<
    ExactlyOneProp<{ 100: Color, AppBackground: Color }> &
    ExactlyOneProp<{ 200: Color, ContentBackground: Color }> &
    ExactlyOneProp<{ 300: Color, CardBackground: Color }> &
    ExactlyOneProp<{ 400: Color, InteractiveBackground: Color }> &
    { link: Color; red: Color; yellow: Color; green: Color; blue: Color }
>;

这就变成了一个2 × 2 × 2 × 2 × 1 = 16个成员的联合,每个成员有2 + 2 + 2 + 2 + 5 = 13个属性,所以在这里一次显示它太多了。它看起来像:

type ThemeMapping = {
    100: Color; AppBackground?: undefined;
    200: Color; ContentBackground?: undefined;
    300: Color; CardBackground?: undefined;
    400: Color; InteractiveBackground?: undefined;
    link: Color; red: Color; yellow: Color; green: Color; blue: Color;
} | {
    100: Color; AppBackground?: undefined;
    200: Color; ContentBackground?: undefined;
    300: Color; CardBackground?: undefined;
    InteractiveBackground: Color; 400?: undefined;
    link: Color; red: Color; yellow: Color; green: Color; blue: Color;
} | ⋯;

您可以通过尝试不同的可能性来验证它是否按预期运行:

declare const c: Color;
const base = { link: c, red: c, yellow: c, green: c, blue: c };
let t: ThemeMapping;
t = { ...base, "100": c, "200": c, "300": c, "400": c }; // okay
t = { ...base, "100": c, "200": c, "CardBackground": c, "400": c }; // okay
t = { ...base, "100": c, "200": c, "400": c }; // error, missing prop
t = { ...base, "100": c, "200": c, "300": c, 
  "CardBackground": c, "400": c }; // error, extra prop

正如你所希望的,你必须定义300CardBackground属性中的一个,如果你试图定义两个或两个都不定义,你会得到一个错误。如果你愿意,你可以自己测试其他属性。
Playground链接到代码

相关问题