typia
typia copied to clipboard
Clean run compiles incomplete validators.
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>()
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
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;
};
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.
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
Were you able to reproduce it?