typia icon indicating copy to clipboard operation
typia copied to clipboard

Clean run compiles incomplete validators.

Open SupremeTechnopriest opened this issue 1 year ago • 3 comments

Bug Report

📝 Summary

Write a short summary of the bug in here.

  • Typia Version: 5.3.1
  • Expected behavior: Validators should generate correctly from an empty directory
  • Actual behavior: Validators are generated incomplete when they have not been generated before. It requires a second run.

During my build step I delete the generated scripts folder after the build is complete so it is excluded from my fuzzy finder and diagnostics. When I run generate without having run it before (clean slate), my union types are not compiled into the validator functions. I have to run it again with the directory and generated code already existing on the file system to get union types to compile. It seems inconsistent but usually takes 2-3 runs to get it to compile properly.

⏯ Playground Link

This does not happen in the playground. Only from a clean slate.

💻 Code occuring the bug

import typia from "typia";

type Test = 'foo' | 'bar' | 'baz' // This is an example, the real code has about 12 valid strings.

interface ITest {
  test: string & Test
}

typia.createValidateEquals<ITest>()

SupremeTechnopriest avatar Nov 28 '23 02:11 SupremeTechnopriest

I have been trying to get a minimal reproduction of this, but I think it might have something to do with the number of validations there are in my project (there are a lot).

Here is the generated validator when it fails:

        body: (input: any): typia.IValidation<IRequestBody> => {
            const errors = [] as any[];
            const __is = (input: any, _exceptionable: boolean = true): input is IRequestBody => {
                const $io0 = (input: any, _exceptionable: boolean = true): boolean => "string" === typeof input.start && /^(\d{4})-(\d{2})-(\d{2})$/.test(input.start) && ("string" === typeof input.end && /^(\d{4})-(\d{2})-(\d{2})$/.test(input.end)) && true && true && (4 === Object.keys(input).length || Object.keys(input).every((key: any) => {
                    if (["start", "end", "metric", "analysis"].some((prop: any) => key === prop))
                        return true;
                    const value = input[key];
                    if (undefined === value)
                        return true;
                    return false;
                }));
                return "object" === typeof input && null !== input && $io0(input, true);
            };
            if (false === __is(input)) {
                const $report = (typia.createValidateEquals as any).report(errors);
                ((input: any, _path: string, _exceptionable: boolean = true): input is IRequestBody => {
                    const $join = (typia.createValidateEquals as any).join;
                    const $vo0 = (input: any, _path: string, _exceptionable: boolean = true): boolean => ["string" === typeof input.start && (/^(\d{4})-(\d{2})-(\d{2})$/.test(input.start) || $report(_exceptionable, {
                            path: _path + ".start",
                            expected: "string & Format<\"date\">",
                            value: input.start
                        })) || $report(_exceptionable, {
                            path: _path + ".start",
                            expected: "(string & Format<\"date\">)",
                            value: input.start
                        }), "string" === typeof input.end && (/^(\d{4})-(\d{2})-(\d{2})$/.test(input.end) || $report(_exceptionable, {
                            path: _path + ".end",
                            expected: "string & Format<\"date\">",
                            value: input.end
                        })) || $report(_exceptionable, {
                            path: _path + ".end",
                            expected: "(string & Format<\"date\">)",
                            value: input.end
                        }), true, true, 4 === Object.keys(input).length || (false === _exceptionable || Object.keys(input).map((key: any) => {
                            if (["start", "end", "metric", "analysis"].some((prop: any) => key === prop))
                                return true;
                            const value = input[key];
                            if (undefined === value)
                                return true;
                            return $report(_exceptionable, {
                                path: _path + $join(key),
                                expected: "undefined",
                                value: value
                            });
                        }).every((flag: boolean) => flag))].every((flag: boolean) => flag);
                    return ("object" === typeof input && null !== input || $report(true, {
                        path: _path + "",
                        expected: "IRequestBody",
                        value: input
                    })) && $vo0(input, _path + "", true) || $report(true, {
                        path: _path + "",
                        expected: "IRequestBody",
                        value: input
                    });
                })(input, "$input", true);
            }
            const success = 0 === errors.length;
            return {
                success,
                errors,
                data: success ? input : undefined
            } as any;
        }

And here is the validator after its run a second time (without deleting the generated folder)

        body: (input: any): typia.IValidation<IRequestBody> => {
            const errors = [] as any[];
            const __is = (input: any, _exceptionable: boolean = true): input is IRequestBody => {
                const $io0 = (input: any, _exceptionable: boolean = true): boolean => "string" === typeof input.start && /^(\d{4})-(\d{2})-(\d{2})$/.test(input.start) && ("string" === typeof input.end && /^(\d{4})-(\d{2})-(\d{2})$/.test(input.end)) && ("productViewRate" === input.metric || "categoryViewRate" === input.metric || "pageViews" === input.metric || "productViews" === input.metric || "categoryViews" === input.metric || "searchViews" === input.metric || "distinctUsers" === input.metric || "distinctSessions" === input.metric || "newUserRate" === input.metric || "returningUserRate" === input.metric || "newUsers" === input.metric || "returningUsers" === input.metric || "avgCacheHitRate" === input.metric || "engagedUsers" === input.metric || "engagedUserRate" === input.metric) && ("adSpend" === input.analysis || "northStar" === input.analysis) && (4 === Object.keys(input).length || Object.keys(input).every((key: any) => {
                    if (["start", "end", "metric", "analysis"].some((prop: any) => key === prop))
                        return true;
                    const value = input[key];
                    if (undefined === value)
                        return true;
                    return false;
                }));
                return "object" === typeof input && null !== input && $io0(input, true);
            };
            if (false === __is(input)) {
                const $report = (typia.createValidateEquals as any).report(errors);
                ((input: any, _path: string, _exceptionable: boolean = true): input is IRequestBody => {
                    const $join = (typia.createValidateEquals as any).join;
                    const $vo0 = (input: any, _path: string, _exceptionable: boolean = true): boolean => ["string" === typeof input.start && (/^(\d{4})-(\d{2})-(\d{2})$/.test(input.start) || $report(_exceptionable, {
                            path: _path + ".start",
                            expected: "string & Format<\"date\">",
                            value: input.start
                        })) || $report(_exceptionable, {
                            path: _path + ".start",
                            expected: "(string & Format<\"date\">)",
                            value: input.start
                        }), "string" === typeof input.end && (/^(\d{4})-(\d{2})-(\d{2})$/.test(input.end) || $report(_exceptionable, {
                            path: _path + ".end",
                            expected: "string & Format<\"date\">",
                            value: input.end
                        })) || $report(_exceptionable, {
                            path: _path + ".end",
                            expected: "(string & Format<\"date\">)",
                            value: input.end
                        }), "productViewRate" === input.metric || "categoryViewRate" === input.metric || "pageViews" === input.metric || "productViews" === input.metric || "categoryViews" === input.metric || "searchViews" === input.metric || "distinctUsers" === input.metric || "distinctSessions" === input.metric || "newUserRate" === input.metric || "returningUserRate" === input.metric || "newUsers" === input.metric || "returningUsers" === input.metric || "avgCacheHitRate" === input.metric || "engagedUsers" === input.metric || "engagedUserRate" === input.metric || $report(_exceptionable, {
                            path: _path + ".metric",
                            expected: "(\"avgCacheHitRate\" | \"categoryViewRate\" | \"categoryViews\" | \"distinctSessions\" | \"distinctUsers\" | \"engagedUserRate\" | \"engagedUsers\" | \"newUserRate\" | \"newUsers\" | \"pageViews\" | \"productViewRate\" | \"productViews\" | \"returningUserRate\" | \"returningUsers\" | \"searchViews\")",
                            value: input.metric
                        }), "adSpend" === input.analysis || "northStar" === input.analysis || $report(_exceptionable, {
                            path: _path + ".analysis",
                            expected: "(\"adSpend\" | \"northStar\")",
                            value: input.analysis
                        }), 4 === Object.keys(input).length || (false === _exceptionable || Object.keys(input).map((key: any) => {
                            if (["start", "end", "metric", "analysis"].some((prop: any) => key === prop))
                                return true;
                            const value = input[key];
                            if (undefined === value)
                                return true;
                            return $report(_exceptionable, {
                                path: _path + $join(key),
                                expected: "undefined",
                                value: value
                            });
                        }).every((flag: boolean) => flag))].every((flag: boolean) => flag);
                    return ("object" === typeof input && null !== input || $report(true, {
                        path: _path + "",
                        expected: "IRequestBody",
                        value: input
                    })) && $vo0(input, _path + "", true) || $report(true, {
                        path: _path + "",
                        expected: "IRequestBody",
                        value: input
                    });
                })(input, "$input", true);
            }
            const success = 0 === errors.length;
            return {
                success,
                errors,
                data: success ? input : undefined
            } as any;
        }

The interface is:

import typia, { tags } from 'typia';

type ACQMetrics = 
  'productViewRate' |
  'categoryViewRate' |
  'pageViews' |
  'productViews' |
  'categoryViews' |
  'searchViews' |
  'distinctUsers' |
  'distinctSessions' |
  'newUserRate' |
  'returningUserRate' |
  'newUsers' |
  'returningUsers' |
  'avgCacheHitRate' |
  'engagedUsers' |
  'engagedUserRate'

type AnalysisType = 'adSpend' | 'northStar'

interface IRequestBody {
  /**
   * The start date.
   */
  start: string & tags.Format<'date'>
  /**
   * The end date.
   */
  end: string & tags.Format<'date'>
  /**
   * The metric to optimize.
   */
  metric: string & ACQMetrics
  /**
   * Return response in natural language.
   */
  analysis: string & AnalysisType
}

I tried making a stackblitz to reproduce, but have been unsuccessful. Which leads me to believe that it has to do with the amount of validators in the project.

https://stackblitz.com/edit/stackblitz-starters-yvy1w4?file=src%2Findex.ts

SupremeTechnopriest avatar Nov 28 '23 23:11 SupremeTechnopriest

You've defined an intersection type of string and constant string literal type.

Following the definition of the intersection type, it is not bug, but a spec. As string covers the ('foo' | 'bar' | 'baz') type, string & ('foo' | 'bar' | 'baz') being converted to the ('foo' | 'bar' | 'baz') is exactly what I've intended. For reference, if you change the type to be union type like string | 'foo' | 'bar' | 'baz', it would converted to the string type in the same reason; string type can cover every constant string literal types.

TypeScript Source Code

import typia from "typia";

typia.createIs<string & ('foo' | 'bar' | 'baz')>();
typia.createIs<string | 'foo' | 'bar' | 'baz'>();

Compiled JavaScript File

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const typia_1 = __importDefault(require("typia"));
input => {
    return "foo" === input || "bar" === input || "baz" === input;
};
input => {
    return "string" === typeof input;
};

samchon avatar Nov 30 '23 07:11 samchon

Yes it all looks good on that front. The problem is the union isn’t being compiled into the validator on the first run when there are no generated files. I’m still trying to reproduce it in a minimal example for you. If I don’t delete the generated directory after the build it seems to work ok, so that’s what I’m doing for now until I am able to figure out a reproduction.

SupremeTechnopriest avatar Nov 30 '23 08:11 SupremeTechnopriest

How about use unplugin-typia so that avoid the generation?

I have no clear way to solve this problem.

https://typia.io/docs/setup/#unplugin-typia

samchon avatar Jun 12 '24 14:06 samchon

Were you able to reproduce it?

SupremeTechnopriest avatar Jun 12 '24 16:06 SupremeTechnopriest