Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

should ow.string.equals be narrowing the type to specific strings? #235

Open
atomanyih opened this issue Mar 10, 2022 · 1 comment
Open

Comments

@atomanyih
Copy link

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 ? ? ?

@YElyousfi
Copy link

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>;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants