ow
ow copied to clipboard
should `ow.string.equals` be narrowing the type to specific strings?
Problem
Use case: validating tagged json objects coming over the wire
type Carrot = {
type: 'carrot';
};
type Hotdog = {
type: 'hotdog';
};
type Food = Hotdog | Carrot;
function validateFood(mightBeFood: unknown): Food {
ow(
mightBeFood,
ow.any(
ow.object.exactShape({
type: ow.string.equals('carrot'),
}),
ow.object.exactShape({
type: ow.string.equals('hotdog'),
}),
),
);
return mightBeFood;
// TS2322: Type '{ type: string; } | { type: string; }' is not assignable to type 'Food'.
// Type '{ type: string; }' is not assignable to type 'Food'.
// Type '{ type: string; }' is not assignable to type 'Hotdog'.
// Types of property 'type' are incompatible.
// Type 'string' is not assignable to type '"hotdog"'.
}
Current workaround:
As far as I could tell, there's no built in way to do this as StringPredicate#addValidator
and StringPredicate#validate
don't seem to let you change the type contained in the predicate.
So, I created a custom predicate:
// exactString.ts
import { Predicate } from 'ow';
class ExactStringPredicate<S extends string> extends Predicate<S> {
expected: S;
constructor(expected: S) {
super('string');
this.expected = expected;
}
equals(): Predicate<S> {
return this.addValidator({
message: (value, label) =>
`Expected ${label} to be \`${this.expected}\`, got \`${value}\``,
validator: (value): value is S => {
return value === this.expected;
},
});
}
}
// these shenanigans are to make it so that we don't have to new this up every time
const exactString = <S extends string>(expected: S) =>
new ExactStringPredicate(expected).equals();
export default exactString;
Possible solutions:
A. Generify ow.string.equals
Currently this doesn't allow any further narrowing:
class StringPredicate extends Predicate<string> {
equals(expected: string): this;
}
It could possibly be:
class StringPredicate extends Predicate<string> {
equals<S extends string>(expected: S): Predicate<S>;
}
This does prevent any further chaining onto that predicate, but one could argue that a string matching equals
cannot be further validated. Certainly none of the other included validators can give you any more useful information
// this no longer works
ow.string.equals('hotdog').includes('dog)
B. Generify StringPredicate
If one wanted to maintain chaining, could do something like this:
class StringPredicate<S extends string> extends Predicate<S> {
equals<S2 extends string>(expected: S2): StringPredicate<S2>;
}
Thus the returned type would still be a StringPredicate
.
C. Some other better solution ? ? ?
I agree with this I think the same can be concept can be applied to .oneOf
// A
class StringPredicate extends Predicate<string> {
oneOf<S extends string>(expected: S[]): Predicate<S>;
}
// B
class StringPredicate<S extends string> extends Predicate<S> {
oneOf<S2 extends string>(expected: S2[]): StringPredicate<S2>;
}