推断地图中键的类型(同时也在地图中键入值)

Inferring type of keys in a map (while also typing the values in a map)

我想从映射中推断键的类型。

我能成功做到:

const componentStyles = {
  button: { color: 'red' },
  heading: { fontSize: 18, lineHeight: 28 },
  body: { fontSize: 12, lineHeight: 18 },
};

type ComponentName = keyof (typeof componentStyles);

TypeScript 会推断:

type ComponentName = 'button' | 'heading' | 'body';

但是,我还想在我的地图中强制执行值的类型。但是当我这样做时:

interface Style {
  color?: string;
  fontSize?: number;
  lineHeight?: number;
}

const componentStyles: {[key: string]: Style} = {
  button: { color: 'red' },
  heading: { fontSize: 18, lineHeight: 28 },
  body: { fontSize: 12, lineHeight: 18 },
};

type ComponentName = keyof (typeof componentStyles);

然后 TypeScript 将推断:

type ComponentName = string | number;

有办法解决这个问题吗? (无需手动写出地图键。)

对于这个用例,我会推荐一个辅助函数,它不会改变 componentStyles 的推断类型,但只允许您创建具有正确 属性 类型的对象:

const asComponentStyles = <T extends Record<keyof T, Style>>(t: T) => t;

这是一个 generic function where the type parameter T is constrained to be assignable to Record<keyof T, Style>. This is a self-referential constraint (known as F-bounded polymorphism) 并允许编译器验证从调用函数推断出的类型 T 对于 keyof T 中的任何键具有可分配给 Style。让我们看看实际效果:

const componentStyles = asComponentStyles({
    button: { color: 'red' },
    heading: { fontSize: 18, lineHeight: 28 },
    body: { fontSize: 12, lineHeight: 18 },
});

type ComponentName = keyof (typeof componentStyles);
// type ComponentName = "button" | "heading" | "body"

这符合您的预期。让我们确保它在防止不良 Style 属性方面发挥作用:

const errorChecking = asComponentStyles({
    okay: { color: 'chartreuse', lineHeight: 123 },
    badColor: { color: 123, lineHeight: 123 }, // error!
    //          ~~~~~ <── 'number' is not assignable to type 'string | undefined'.
    excessPropChecking: { colour: "grey" } // error!
    //   ┌──────────────> ~~~~~~~~~~~~~~
    // Object literal may only specify known properties, 
    // but 'colour' does not exist in type 'Style'.
    // Did you mean to write 'color'?
})

看起来也不错。


请注意,您可以使用类似于您在问题中的操作方式的索引签名来使用更简单的非自引用辅助函数:

const asComponentStylesIndex = <T extends { [k: string]: Style }>(t: T) => t;

const componentStylesIndex = asComponentStylesIndex({
    button: { color: 'red' },
    heading: { fontSize: 18, lineHeight: 28 },
    body: { fontSize: 12, lineHeight: 18 },
}); // okay

这是有效的,因为 TypeScript 会接受一个对象文字并给它一个 implicit index signature

但如果您要使用接口值,我不推荐这样做,因为 they are not currently allowed to gain implicit index signatures,而 F-bounded 版本适用于接口和对象文字:

interface MyComponent {
    foo: Style,
    bar: Style
}
declare const myComponent: MyComponent; // value of an interface type

const works = asComponentStyles(myComponent); // okay
const doesntWork = asComponentStylesIndex(myComponent); // error!
//  ┌───────────────────────────────────> ~~~~~~~~~~~
// Index signature is missing in type 'MyComponent'.

不确定接口值的东西是否是您的用例的一部分。


总之,希望对您有所帮助;祝你好运!

Link to code