打字稿:断言未知输入具有指定键的 Pick<ConcreteType, subset of keys of ConcreteType> 类型

Typescript: Assert unknown input has type Pick<ConcreteType, subset of keys of ConcreteType> for specified keys

在尝试创建通用函数来测试未知输入是否是已知对象类型的子集时,我 运行 遇到了 Typescript 的麻烦。我想指定应该存在哪些键并断言输入的类型为 Pick 键的子集。我的主张

简化代码:

type Rectangle = {
  width: number,
  height: number
}

const assertObject: (o: unknown) => asserts o is Record<PropertyKey, unknown> = (
  o,
) => {
  if (typeof o !== `object`) {
    throw new Error();
  }
};

function assertRectangle <K extends keyof Rectangle>(o: unknown, ...keys: K[]): asserts o is Pick<Rectangle, K>  {
  assertObject(o);
  // >>>>>>>>>>>>>>>>>>>>>> HERE <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  if (keys.includes(`width` as K) && !o.hasOwnProperty('width')) {
    throw new Error('');
  }
  if (keys.includes(`height` as K) && !o.hasOwnProperty('height')) {
    throw new Error('');
  }
}

const rect = {width: 1, height: 1};
assertRectangle(rect, 'width'); // Pick<Rectangle, "width">
assertRectangle(rect, 'height', 'width'); // Pick<Rectangle, "height" | "width">

此代码有效,但如果我们删除 keys.includes 中的 as K 则无效。

if (keys.includes(`width`) && !o.hasOwnProperty('width')) {
   throw new Error('');
}
// OR:
if (keys.includes(`width` as const) && !o.hasOwnProperty('width')) {
   throw new Error('');
}

Argument of type '"width"' is not assignable to parameter of type 'K'. '"width"' is assignable to the constraint of type 'K', but 'K' could be instantiated with a different subtype of constraint 'keyof Rectangle'.ts(2345)

我想知道为什么 as const 在这里不起作用,我想知道的问题是当我或同事决定更改或重命名 Rectangle 类型的属性时,我希望 typescript 发出警告当这个断言不再涵盖类型时我。添加、重命名或减去类型上的 属性 不会被此断言捕获。

您在评论中说:

I am doing it in this way because each property/key could have seperate checks. For example the Rectangle can have an id field and I would also want to assert if that id is of type string and matches a uuid regex

在那种情况下,我会这样处理:

  1. 遍历keys检查它们是否存在

  2. 对于需要额外检查的属性,使用 if/else if 来识别它们并应用额外检查。 (我很惊讶地发现 switch 不会抱怨永远无法达到的情况,但 if 会抱怨。)

类似于以下内容——请注意,在此示例中,我检查了 Rectangle 上不存在的 属性,并且 TypeScript 会警告我这一点。这是为了演示您的 “我想知道的是,当我或同事决定更改或重命名 Rectangle 类型的属性时” 情况(在这种情况下,假设 Rectangle 曾经有 id 但现在没有了)。

type Rectangle = {
    width: number,
    height: number;
};

const assertObject: (o: unknown) => asserts o is Record<PropertyKey, unknown> = (
    o,
) => {
    if (o === null || typeof o !== `object`) {
//      ^^^^^^^^^^^^^−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− added
        throw new Error();
    }
};

function assertRectangle<K extends keyof Rectangle>(
    o: unknown,
    ...keys: K[]
): asserts o is Pick<Rectangle, K> {
    assertObject(o);
    for (const key of keys) {
        // Basic check
        if (!o.hasOwnProperty(key)) {
            throw new Error("");
        }
        // Additional per-property checks
        // (I was surprised that `switch` didn't work here to call out property
        // names that aren't on Rectangle like "id" below.)
        if (key === "width" || key === "height") {
            if (typeof o[key] !== "number") {
                throw new Error(`typeof of '${key}' expected to be 'number'`);
            }
        } else if (key === "id") {
//                 ^^^^^^^^^^^^−−−−−−−− causes error because `id` isn't a valid
//                                      Rectangle property (e.g., if you remove
//                                      a property from `Rectangle`, TypeScript
//                                      warns you)
            const id = o[key];
            if (typeof id !== "string" || !id) {
                throw new Error(`'${key}' expected to be non-empty string`);
            }
        }
    }
}

declare let rect1: unknown;
declare let rect2: unknown;
declare let rect3: unknown;
assertRectangle(rect1, "width");
rect1; // <== type is Pick<Rectangle, "width">
assertRectangle(rect2, "height", "width");
rect2; // <== type is Pick<Rectangle, "height" | "width">
assertRectangle(rect3, "height", "width", "not-rectangle-property");
// Error −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−^

Playground link