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

[Feature] plugins to enhance type validation #34

Open
mdesousa opened this issue Jan 12, 2021 · 5 comments
Open

[Feature] plugins to enhance type validation #34

mdesousa opened this issue Jan 12, 2021 · 5 comments

Comments

@mdesousa
Copy link

Thanks for this awesome library! It is very well thought out.

It would be great if it could provide support to inject additional logic to validate certain types.
For example, given the type below:

type Iso8601Date = string;

I would like to add logic that verifies that the value is a valid date formatted as "YYYY-MM-DD".
I can see many other scenarios where similar validations would be useful. For example, for certain numbers you may want to verify that they are within a specific range (e.g. a "Percent" type would need to be between 0 and 100). Certain string types may need to be checked for length. An Email or PhoneType could be checked with a RegEx... etc.

@dsagal
Copy link
Member

dsagal commented Jan 12, 2021

It's possible already! Something like this should work:

import {assert} from "chai";
import {BasicType, createCheckers} from "..";
import * as t from "..";
// tslint:disable:object-literal-key-quotes

export type Iso8601Date = string;
export interface Foo {
  date: Iso8601Date;
  event: string;
}

namespace sample {
  export const Iso8601Date = t.name("string");
  export const Foo = t.iface([], {
    "date": "Iso8601Date",
    "event": "string",
  });
  export const exportedTypeSuite: t.ITypeSuite = {
    Iso8601Date,
    Foo,
  };
}

function checkIsoDate(v: unknown): boolean {
  return typeof v === 'string' && /^\d\d\d\d-\d\d-\d\d$/.test(v) && Boolean(Date.parse(v));
}

const checkers = createCheckers({
  ...sample.exportedTypeSuite,
  Iso8601Date: new BasicType(checkIsoDate, "is not an ISO8601 date"),
});

// Type of a field is invalid.
checkers.Foo.check({event: "foo", date: '2021-01-12'});
assert.throws(() => checkers.Foo.check({event: "foo", date: 'bar'}));
assert.throws(() => checkers.Foo.check({event: "foo", date: '2020-50-50'}));

To make it generally available, you could import the object basicTypes and set basicTypes.Iso8601Date = new BasicType(checkIsoDate, "is not an ISO8601 date").

@mdesousa
Copy link
Author

Excellent, thanks a lot! This works great. Just to document what I did... I separated types that need custom checking from other types into their own file. In our build process, we are not invoking ts-interface-builder for this file.
We export the types and the ITypeSuite object which can be consumed by other modules similarly to suites generated by ts-interface-builder. This is the file with the types:

// types.ts
import * as t from "ts-interface-checker";

type DateIso8601 = string;

const checkIsoDate = (v: unknown): boolean =>
  typeof v === "string" &&
  /^\d\d\d\d-\d\d-\d\d$/.test(v) &&
  Boolean(Date.parse(v));

const checkers: t.ITypeSuite = {
  DateIso8601: new t.BasicType(checkIsoDate, "is not an ISO8601 date"),
};

export default checkers;
export type { DateIso8601 };

Other types with interfaces etc. that just require regular checks:

ts-interface-builder ./my-types.ts

Now to consume it:

import myTypesTI from "./my-types-ti";
import coreTI from "./types";

const checkers = createCheckers(coreTI, myTypesTI);
checkers.MyType.check(x);

@mdesousa
Copy link
Author

Hi @dsagal , I have a follow up question. I'm wondering if there is a way create a checker for a more complex type that invokes other checkers. For example, for type MyObjectList below I would like to verify that all the id values are unique.

interface IMyObject {
  id: string;
  // other properties...
}

type MyObjectList = IMyObject[];

Thanks!

@mdesousa
Copy link
Author

Ok... I think I figured it out. Let me know if the code below looks about right...
_TI is imported from the type-ti.ts generated with the builder in order to perform base type checks.

class TMyObjectList extends t.TType {
  public getChecker(suite: t.ITypeSuite, strict: boolean): CheckerFunc {
    const ttype = _TI.TMyObjectList;
    const itemChecker = ttype.getChecker(suite, strict);
    return (value: any, ctx: IContext) => {
      const ok = itemChecker(value, ctx);
      if (!ok) return ctx.fail(null, null, 1);
      const ids = value.map((x: { id: string }) => x.id);
      const hasDuplicates = new Set(ids).size !== ids.length;
      if (hasDuplicates) return ctx.fail(null, "has duplicates", 0);
      return true;
    };
  }
}

@dsagal
Copy link
Member

dsagal commented Jan 17, 2021

Yup, this looks like it should work!

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

No branches or pull requests

2 participants