网站搜索

javascript - 在 TypeScript 中为动态键指定最大属性数量的类型


我在想在 TypeScript 中是否有可能对对象中动态属性的最大数量进行类型限制。

所以基本的例子是用于跟踪事件:

events.track('SOME_EVENT', { first: 'a', other: 'b', some: 'c'})

事件数据应包含最多3个属性及其各自的值,键也可以是动态的。

我用一个基本的 Record 类型来编写,但允许的属性数量没有限制:

export interface Events {
  track: (name: string, params?: Record<string, string | number | unknown>) => void;
}

这可能吗?

答案#1

我在这里使用 TuplifyUnion 找到了解决方案:

我不确定这有多“安全”(请参见免责声明)。使用 TuplifyUnion 被认为是不安全的,因为顺序可能随时变化。由于这里顺序并不重要,这里仅关心元素的数量,我认为在这里使用是可以的。

该解决方案允许 0-3 个键。如果你想要其他数量,只需将它们添加到联合类型中(例如 1 | 2 接受 1 或 2 个键)。

type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
type LastOf<T> =
  UnionToIntersection<T extends any ? () => T : never> extends () => (infer R) ? R : never

type Push<T extends any[], V> = [...T, V];

type TuplifyUnion<T, L = LastOf<T>, N = [T] extends [never] ? true : false> =
  true extends N ? [] : Push<TuplifyUnion<Exclude<T, L>>, L>


type MaxThreeProperties<T extends Record<string, any>> =
  TuplifyUnion<keyof T>["length"] extends 0 | 1 | 2 | 3 ? T : never
//    add all acceptable key amounts here ^   ^   ^   ^

function track<T extends Record<string, any>>(
  name: string, 
  params: MaxThreeProperties<T>
) {}

我们基本上把所有键放入一个元组中,然后“手动”检查元组的长度。虽然可能会变得很乱,但这可以很容易地扩展到其他数量的属性。

不过,有一个缺点是会出现错误信息:

类型 'string' 不能分配给类型 'never'。(2322)

这对于使用该函数的人来说可能会令人困惑...


这里有一些测试:

track('SOME_EVENT', {})                                  // works
track('SOME_EVENT', {a: ""})                             // works
track('SOME_EVENT', {a: "", b: ""})                      // works
track('SOME_EVENT', {a: "", b: "", c: ""})               // works
track('SOME_EVENT', {a: "", b: "", c: "", d: ""})        // ERROR
track('SOME_EVENT', {a: "", b: "", c: "", d: "", e: ""}) // ERROR

const a = {a: "", b: "", c: ""}
const b = {a: "", b: "", c: "", d: ""}

track('SOME_EVENT', a)  // works
track('SOME_EVENT', b)  // ERROR

操场

答案#2

我想不出一种方法来声明一个对象接口,该接口有 1 到 3 个(但只能是 1 到 3 个)未知的字符串命名属性(但这并不意味着没有这种方法;我在 TypeScript 上只处于中级水平)。

我倾向于使用元组的联合:

type EventParam = [name: string, value: string | number | unknown];
type EventParams = 
      [EventParam]
    | [EventParam, EventParam]
    | [EventParam, EventParam, EventParam];
export interface Events {
    track: (name: string, params?: EventParams) => void;
}

declare let events: Events;
// Works with 1:
events.track("something", [["first", "a"]]);
// Works with 2:
events.track("something", [["first", "a"], ["other", "b"]]);
// Works with 3:
events.track("something", [["first", "a"], ["other", "b"], ["some", "c"]]);
// Fails with 4:
events.track("something", [["first", "a"], ["other", "b"], ["some", "c"], ["fourth", 42]]);
// Error as desired −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^

操场链接

但那可能没有最佳的人体工学设计。