rhys-vdw / ts-auto-guard

Generate type guard functions from TypeScript interfaces

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Optional properties ignored when strictNullChecks is disabled

flixcheck opened this issue · comments

Hi!

First of all: great package! I'm really looking forward to use it for validating incoming "any" objects.
Unfortunately, it doesn't work as expected / described:

My interface ...

/** @see {isProduct} ts-auto-guard:type-guard */
export interface IProduct {
    id?: string;
    billingType?: ProductBillingType;
    defaultBillingMode?: ProductBillingMode;
    defaultPrice?: IPrice;
    defaultIncreaseDeposit?: IUsageItems;
    defaultIncreaseInclusive?: IUsageItems;
    title?: string;
    shortDescription?: string;
}

... results in this type guard function:

function isProduct(obj, _argumentName) {
    return ((obj !== null &&
        typeof obj === "object" ||
        typeof obj === "function") &&
        typeof obj.id === "string" &&
        (obj.billingType === product_interface_1.ProductBillingType.Recurring ||
            obj.billingType === product_interface_1.ProductBillingType.OneTime) &&
        (obj.defaultBillingMode === product_interface_1.ProductBillingMode.Basic ||
            obj.defaultBillingMode === product_interface_1.ProductBillingMode.PerAccount ||
            obj.defaultBillingMode === product_interface_1.ProductBillingMode.PerUser ||
            obj.defaultBillingMode === product_interface_1.ProductBillingMode.PerSpecifiedUser) &&
        (obj.defaultPrice !== null &&
            typeof obj.defaultPrice === "object" ||
            typeof obj.defaultPrice === "function") &&
        typeof obj.defaultPrice.fixed === "number" &&
        Array.isArray(obj.defaultPrice.contractBasedPrices) &&
        obj.defaultPrice.contractBasedPrices.every(function (e) {
            return (e !== null &&
                typeof e === "object" ||
                typeof e === "function") &&
                typeof e.templateContractId === "string" &&
                typeof e.price === "number";
        }) &&
        (obj.defaultIncreaseDeposit !== null &&
            typeof obj.defaultIncreaseDeposit === "object" ||
            typeof obj.defaultIncreaseDeposit === "function") &&
        typeof obj.defaultIncreaseDeposit.check === "number" &&
        typeof obj.defaultIncreaseDeposit.automatedCheck === "number" &&
        typeof obj.defaultIncreaseDeposit.signature === "number" &&
        typeof obj.defaultIncreaseDeposit.sms === "number" &&
        typeof obj.defaultIncreaseDeposit.email === "number" &&
        (obj.defaultIncreaseInclusive !== null &&
            typeof obj.defaultIncreaseInclusive === "object" ||
            typeof obj.defaultIncreaseInclusive === "function") &&
        typeof obj.defaultIncreaseInclusive.check === "number" &&
        typeof obj.defaultIncreaseInclusive.automatedCheck === "number" &&
        typeof obj.defaultIncreaseInclusive.signature === "number" &&
        typeof obj.defaultIncreaseInclusive.sms === "number" &&
        typeof obj.defaultIncreaseInclusive.email === "number" &&
        typeof obj.title === "string" &&
        typeof obj.shortDescription === "string");
}

As you can see, e.g. the typeof obj.id === "undefined" ist missing.

Did I do some fault while using it? I would expect to have that undefined-case implemented since it can be seen in the README (the age property).

Best
Michael

Hi Michael,

Actually that case is covered here, so it should work.

Not totally sure, but this behaviour might be a result of not having strictNullChecks enabled, as all fields are potentially undefined in this mode.

Let me know if this is the case. Whatever the problem is here this behaviour is clearly wrong!

Hoi Rhys!

Yes, you are absolutely right. That fixed it.

Thank you also for the quick response!

Reopening this issue because this is confusing behaviour. Possible fixes:

  1. Override that option when generating types.
  2. Show a warning of some sort.
  3. Make everything optional outside of strict mode?
  4. Add a note in the docs to turn this on in your project...

We didn't have any issues when adding strictNullChecks but I assume that other developers might run into trouble if the have to activate it for this library to work, thus fix 2 and 4 are not preferable in my opinion. I'd vote for 1! 😊

Just to hopefully save people some time, if you don't have strict: true in your tsconfig.json, you'll have the same problem with optional fields not being treated as such in generated guards (getting == "string" instead of =="undefined" || =="string").

I'm leaving this here because this issue has "optional" in its title, which is what I searched for when my fields weren't made optional. I didn't realize that my issue was in the Typescript config. Perhaps the simplest way to help out here is to put something in the README? It does mention tsconfig.json, but not explicitly that the configuration influences what is generated. I'd be happy to open a PR.

Perhaps the simplest way to help out here is to put something in the README? It does mention tsconfig.json, but not explicitly that the configuration influences what is generated. I'd be happy to open a PR.

That's fine. I think overriding structNullChecks in the tsconfig is maybe a better fix, but if you want to link to this issue from the README with a warning that should save some people confusion.

This library doesn't really make sense to use without strictNullChecks because it's enforcing guarantees that TS does not give you anyway.

However, if it were to support it, the behaviour is wrong. The check should be:

// without strict null checks
interface Foo {
  foo: string;
  bar: string?;
  }
  
  function isFoo(obj: any): obj is Foo {
    return (
      (!obj || Object.is(obj)) &&
      (!obj.foo || typeof(obj.foo) === "string") &&
      (!obj.bar || typeof(obj.bar) === "string")
    );
  }

But what's even the point?