打字稿:断言未知输入具有指定键的 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
在那种情况下,我会这样处理:
遍历keys
检查它们是否存在
对于需要额外检查的属性,使用 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 −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−^
在尝试创建通用函数来测试未知输入是否是已知对象类型的子集时,我 运行 遇到了 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
在那种情况下,我会这样处理:
遍历
keys
检查它们是否存在对于需要额外检查的属性,使用
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 −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−^