首页 > 解决方案 > 获取泛型对象成员的编译时类型

问题描述

我正在尝试创建一个函数来简化set可用于任意目的的对象上的 ter 函数的设置。明显的用途包括类型保护、值和边界检查或触发事件。

这个想法是获取一个已知形状的对象并具有一个您调用它的函数,该函数返回一个具有相同可枚举形状的新对象,其中每个对象的成员实际具有setget“属性”。setters 来自函数的第二个参数,并且tersget只是返回“受保护”值。

这种方法的大量实用性来自可能的严格类型。当受保护的对象是在单独的代码段中定义的,甚至可能超出您的控制范围时,这尤其有用。如果对象形状发生变化,类型错误将确保set添加/删除新的术语。

我很容易创建了一个“平面”版本。

平面版

function makeObjectSetter<T extends {}>(
  internal: T,
  setters: {
    [P in keyof T]: (next: T[P]) => void;
  }
) {
  const ret = {};

  for (const x in internal) {
    Object.defineProperty(ret, x, {
      set: setters[x],
      get: () => internal[x],
      enumerable: true,
    });
  }

  return ret as T;
}

用法

const myObject = {
  num: 42,
  str: 'initialValue',
};

const protectedObject = makeObjectSetter(myObject, {
  num(x) {
    // Make sure positive
    myObject.num = Math.max(x, 0);
  },
  str(s) {
    // Always double the input
    myObject.str = s + s;
  },
});

console.log(myObject);
// { num: 42, str: 'initialValue' }

protectedObject.num = -1;
protectedObject.str = 'a';

console.log(myObject);
// { num: 0, str: 'aa' }

for (let x in protectedObject) console.log(x);
// num
// str

当目标对象具有嵌套对象时,棘手的部分就出现了。虽然可以使用平面版本,但编写嵌套越深就越麻烦。

所以,我正在尝试编写平面函数的递归版本。这会检测成员的类型是否为对象并以不同方式处理。我相信我的函数签名类型是正确的,但是函数内部在一行中存在一个我无法弄清楚的硬错误。

我知道 TypeScript 类型在运行时不可用,但我相信这是一个编译时问题。我相信如果我在指出的行上得到正确的语法,它将起作用。但也许我错了?

递归版本

type NestedSetters<T extends {}> = { [P in keyof T]: T[P] extends {} ? NestedSetters<T[P]> : (next: T[P]) => void };

function makeObjectSetterRecursive<T extends {}>(internal: T, setters: NestedSetters<T>) {
  const ret = {};

  for (const x in internal) {
    let prop: PropertyDescriptor;

    // Can't figure out this line
    type t = typeof internal[x];

    // Pretty sure this test is the right runtime test for my purposes
    if (typeof internal[x] == 'object') {
      prop = {
        value: makeObjectSetterRecursive(internal[x], setters[x] as NestedSetters<t>), // Should be able to avoid this `as` cast, no?
      };
    } else {
      prop = {
        set: setters[x] as (next: t) => void, // Should be able to avoid this `as` cast, no?
        get: () => internal[x],
      };
    }

    prop.enumerable = true;

    Object.defineProperty(ret, x, prop);
  }

  return ret as T; // Extra extra bonus get rid of this `as` cast
}

此外typeof internal[x],我也尝试过Pick<typeof internal, x>和其他猜测无济于事。

对此的任何想法将不胜感激。答案可能是我想要的不可能。

支线任务:我觉得as使用正确的类型提示不需要强制转换。

标签: typescripttypeshelper

解决方案


正如您所提到的,TypeScript 类型在运行时不可用,因此typeof internal[x]无法工作。您正在寻找的是T[Extract<keyof T, string>],它提取道具值的类型。

关于铸造问题,条件类型缩小似乎存在问题。 https://github.com/microsoft/TypeScript/issues/30152 因此,必须依赖 if else 语句中的运行时逻辑来映射正确的类型(在这种情况下typeof value === 'object')。对于语义,我认为制作用户定义的类型保护isNestedSetters并将值转换为联合类型很有用NestedSetters<T[P]> | Setter<T[P]>,因为编译器可以正确缩小范围。

这是完整版:

type Setter<T> = T extends boolean ? (next: boolean) => void : (next: T) => void

type SetterOrNested<T> = T extends object ? NestedSetters<T> : Setter<T>

type NestedSetters<T> = {
  [P in keyof T]: SetterOrNested<T[P]>
}

function isNestedSetters<T>(value: any): value is NestedSetters<T> {
  return typeof value === 'object';
}

function makeObjectSetterRecursive<T extends {}>(internal: T, setters: NestedSetters<T>) {
  const ret = <T>{};

  for (const x in internal) {
    let prop: PropertyDescriptor;

    type P = Extract<keyof T, string>

    const setterOrNested = setters[x] as NestedSetters<T[P]> | Setter<T[P]>

    if (isNestedSetters<T[P]>(setterOrNested)) {
      prop = {
        value: makeObjectSetterRecursive(internal[x], setterOrNested),
      };
    } else {
      prop = {
        set: setterOrNested,
        get: () => internal[x],
      };
    }

    prop.enumerable = true;

    Object.defineProperty(ret, x, prop);
  }

  return ret;
}

它应该输出以下内容:

const myObject = {
  num: 42,
  str: 'initialValue',
  others: {
    bool: true,
    nestedStr: ''
  }
};

const protectedObject = makeObjectSetterRecursive(myObject, {
  num(x) {
    // Make sure positive
    myObject.num = Math.max(x, 0);
  },
  str(s) {
    // Always double the input
    myObject.str = s + s;
  },
  others: {
    bool(b) {
      // Toggle
      myObject.others.bool = !b
    },
    nestedStr(s) {
      // Add 3 dots
      myObject.others.nestedStr = s + '...'
    }
  }
});

console.log(myObject);
// { num: 42, str: 'initialValue', others: { bool: true, nestedStr: '' } }

protectedObject.num = -1;
protectedObject.str = 'a';
console.log(myObject);
// { num: 0, str: 'aa', others: { bool: true, nestedStr: '' } }

protectedObject.others.bool = true;
protectedObject.others.nestedStr = 'abc';
console.log(myObject);
// { num: 0, str: 'aa', others: { bool: false, nestedStr: 'abc...' } }

我必须说,我不太确定这个用例,但这是一个有趣的概念,所以我还是决定试一试。


推荐阅读