I’m getting an object (type: any) of an external library A and want to pass it to a function from a library B. That function expects a Type B-Input, which is a Union Type (number | string | somethingCustom).
How can I check if the object is compatible?
(More precise: momentJS expects a MomentInput. An Angular AbstractControl.value has type any. I am looking for a type safe way to handle this to be sure that the passed value matches the functions expectations and throw an error otherwise.)
Code Example:
someFunction(obj: any): Moment {
// Sadly, it is unchecked wheter obj fits the input type for moment()
// Linter is unhappy (no-unsafe-argument), disabling linter is unsatisfying
return moment(obj)
}
>Solution :
Unfortunately, you can only do this for the types you know to test for, you can’t discover the types used by MomentJS in its MomentInput type and generate tests at runtime to check for them.
At present, the MomentInput type is:
type MomentInput = Moment | Date | string | number | (number | string)[] | MomentInputObject | null | undefined;
You could write a type predicate to test whether a given value is one of the types in that union. Sadly, this is complicated a bit by the fact that one of the above is MomentInputObject, which is an object type where all 24 properties are optional. So {} is a valid MomentInputObject, and so basically anything goes.
But let’s try it anyway:
// PLEASE NOTE: This is untested and may need tweaking. It's not meant to
// be perfect from the get-go, it's meant to demonstrate the kind of
// function _you'd_ write to do this.
import moment, { isMoment, MomentInput, MomentInputObject } from "moment";
function isMomentInput(value: any): value is MomentInput {
switch (typeof value) {
case "string":
case "number":
case "undefined":
return true;
case "object":
if (value instanceof Date) {
return true;
}
if (Array.isArray(value)) {
return value.every((element) => {
const elementType = typeof element;
return elementType === "string" || elementType === "number";
});
}
return (
value === null || isMoment(moment) || isMomentInputObject(value)
);
}
return false;
}
const momentInputObjectPropNames = [
"years",
"year",
"y",
"months",
"month",
"M",
"days",
"day",
"d",
"dates",
"date",
"D",
"hours",
"hour",
"h",
"minutes",
"minute",
"m",
"seconds",
"second",
"s",
"milliseconds",
"millisecond",
"ms",
];
function isMomentInputObject(value: any): value is MomentInputObject {
// Note: This requires at least one of the named properties.
// Technically, from TypeScript's point of view, that's incorrect,
// `MomentInputObject` allows `{}` because it has no required properties.
// Pragmatically, though, it's probably what you want.
return momentInputObjectPropNames.some((propName) => propName in value);
}
Then your code is:
someFunction(obj: any): Moment {
if (!isMomentInput(obj)) {
// throw new Error(`Invalid Moment input`);
// (Or return something)
}
return moment(obj);
}
Note that the isMomentInput function will need updating if MomentJS changes MomentInput. MomentJS is in maintenance mode so that’s probably not an issue, but still…