When trying to create a generic function to test if an unknown input is a subset of a known object type I run into trouble with Typescript. I want to specify which keys should be present and assert that the input is of type Pick<ConcreteType, subset of keys of ConcreteType>. My assertion
Simplified code:
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">
This code works but not if we remove the as K
inside keys.includes
.
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)
I was wondering why as const
doesn't work here, the problem I am wondering about is that when I or a colleague decide to change or rename properties on the type Rectangle, I would like typescript to warn me when this assert does not longer cover the type. Adding, renaming or subtracting a property on the type will not get catched by this assert.
In a comment you've said:
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
In that case, I'd approach it like this:
Loop through keys
checking that they exist
For properties that need additional checks, use if
/else if
to identify them and apply the extra checks. (I was surprised to find that switch
doesn't complain about cases that can never be reached, but if
does.)
Something like the following — note that in this example I have a check for a property that doesn't exist on Rectangle
, and TypeScript warns me about that. This is to demonstrate your "I am wondering about is that when I or a colleague decide to change or rename properties on the type Rectangle" situation (in this case, let's say Rectangle
used to have id
but doesn't anymore).
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 −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−^