Open sergio-milu opened 9 months ago
What the __nominal__type
type is?
What the
__nominal__type
type is?
oh yes sorry, forgot to copy it
declare const __nominal__type: unique symbol;
but this doesnt seem to be a issue only with this example, tested with this code and same issue
It seems actually nonsense type. Is there any reason why adding the __nominal_type__
in the DTO?
Is it required for client develoers?
It seems actually nonsense type. Is there any reason why adding the
__nominal_type__
in the DTO?Is it required for client develoers?
nop, is client-side we translate those types to just string
, but in our whole backend codebase we use this 'branded' types, it's a convenience, otherwise I'll need to cast those string from DTO to internal user cases.
Also found this , that is the same case that seems resolved, but got same error.
that nominal type works in TS, I took it from this issue
To accomplish your requirement, we need to make a logic that determining whether intersection type is one of nonsensible, or especially allowed. As you know, the type number & object
is basically nonsensible due to number
type cannot be object
type, and object
type neither can't be the number
type.
Previous PR (https://github.com/samchon/typia/pull/657) allowed only when the object
type has only optionial properties. By the way, you wanna make an intersection type that object
side has the required property. In that case, I don't know how to distinguish between insensible or intended. If do you have any special idea, please inform me. If not, recommend to change your object
type to be optional.
I changed my 'branded' types to this
export enum TestType {
__brand = 'Test',
}
export type TestID = string & TestType;
and seems that this works, thanks!
I changed my 'branded' types to this
export enum TestType { __brand = 'Test', } export type TestID = string & TestType;
and seems that this works, thanks!
I've realized another issues arised with this change, and is that the validator fails
err: {
"type": "BadRequestException",
"message": "Request query data is not following the promised type.",
"stack":
BadRequestException: Request query data is not following the promised type.
at /Users/sergiofernandez/Repositories/milu-web/node_modules/@nestia/core/src/decorators/internal/validate_request_query.ts:25:16
at TypedQuery (/Users/sergiofernandez/Repositories/milu-web/node_modules/@nestia/core/src/decorators/TypedQuery.ts:59:31)
at /Users/sergiofernandez/Repositories/milu-web/node_modules/@nestjs/core/helpers/context-utils.js:43:28
at resolveParamValue (/Users/sergiofernandez/Repositories/milu-web/node_modules/@nestjs/core/router/router-execution-context.js:145:31)
at Array.map (<anonymous>)
at pipesFn (/Users/sergiofernandez/Repositories/milu-web/node_modules/@nestjs/core/router/router-execution-context.js:150:45)
at /Users/sergiofernandez/Repositories/milu-web/node_modules/@nestjs/core/router/router-execution-context.js:37:36
at InterceptorsConsumer.transformDeferred (/Users/sergiofernandez/Repositories/milu-web/node_modules/@nestjs/core/interceptors/interceptors-consumer.js:31:33)
at /Users/sergiofernandez/Repositories/milu-web/node_modules/@nestjs/core/interceptors/interceptors-consumer.js:18:86
at AsyncResource.runInAsyncScope (node:async_hooks:203:9)
"response": {
"type": "Object",
"message": "Request query data is not following the promised type.",
"stack":
"path": "$input.memberId",
"reason": "Error on typia.http.assertQuery(): invalid type on $input.memberId, expect to be \"Member\"",
"expected": "\"Member\"",
"value": "d7652fd0-eb23-428a-9c13-ff8a8d9c3495"
},
TSC and nestia's swagger validator is working right and I have the swagger definition working and all TS types, do you know how can handle this error? @samchon
Make the property type to be optional. It is the only one way at now.
If you do not want it, consider and suggest me how to distinguish whether valid or not.
Make the property type to be optional. It is the only one way at now.
If you do not want it, consider and suggest me how to distinguish whether valid or not.
cant do that, cause if optional TS wont plain and will accept any string example
I was thinking in a way to fix this, and came up with an idea
typia could expose a new type tags.Nominal<'MyString>
or tags.Branded<'MyString>
and internally this type will be
type tags.Branded<Identifier extends string> = {
__INTERNAL_TYPIA_BRAND: Identifier;
};
then we can use it the same way we use typia's validatos string & tags.Nomial<'UserID'>
so, with that is possible to do something similar to this PR, detect that special type and ignore it (same way you are ignoring optional properties in object and letting only the sting or number), WDYT?
I think it would better to change your type. Your case seems too domestic and occurs hard coding.
I think it would better to change your type. Your case seems too domestic and occurs hard coding.
thing is that if I change my type I'll break the whole type checking that I have, because I have several Ids (UserID, MemberID, AdminID) and if I use optional object, that is the same as using string for TS (I doesnt make sense to do string & {prop?: number} as that is the same as just string)
so IMHO this is a stopper for everyone using this nominal/branded types
type MyNumber = number & {
__brand__?: Something;
}
If not like above, then no way to validate.
In actually, below type becomes compile error in tsc
.
No way for typia
to support that tsc
is prohibiting.
const value: number & { nested: Record<string, string> } = 3;
type MyNumber = number & { __brand__?: Something; }
If not like above, then no way to validate.
In actually, below type becomes compile error in
tsc
.No way for
typia
to support thattsc
is prohibiting.const value: number & { nested: Record<string, string> } = 3;
dont think this is 'too domestic' this is exposed in really well-known libs type-fest zod
long thread talking about this -> https://twitter.com/mattpocockuk/status/1625173884885401600
would you be open to a PR or some help? we dont want to lose this type-safety in our codebase
Then make the brand type's property to be optional.
typia
cannot support that TypeScript compiler occurs error case.
If TypeScript core team makes it not be compile error, I'll consider your opinion.
As you know, typia
is using the pure Typescript type directly, so no way to supporting prohibited feature of TypeScript.
I'm so confused why you can't just detect the interesction of a string and another type and then only check for it being a string. Nobody wants to do
const branded: Branded = {something: "nothing");
They want to do
const branded: Branded = "string value"
There are tons of things that the typescript community does that you refuse to support that make this library unusable. If you'd like, I can help you fix it. I'd rather use this than Zod.
Like why not detect the string & {}
pattern here:
// ESCAPE WHEN ONLY CONSTANT TYPES EXIST
if (
atomics.size + constants.length + arrays.size > 1 ||
individuals.length + objects.length !== children.length
) {
errors.push({
name: children.map((c) => c.getName()).join(" & "),
explore: { ...explore },
messages: ["nonsensible intersection"],
});
return true;
}
and just allow it?
Branded types are very popular in typescript. I really don't understand the problem you are making this out to be.
@samchon , I think you're missing the point of the "Branding" - a technique that is used within the typescript compiler itself.
Consider the types:
export type Brand<K, T> = K & { __brand: T };
export type Weeks = Brand<number, 'week'>;
export type Days = Brand<number, 'day'>;
let x = 2 as Weeks;
let y = 14 as Days;
function onVacation(n: Weeks) { /* ... */ }
onVacation(x); // Good
onVacation(y); // Error!
The point here is that you can initialise with a type assertion, and have some confidence that you can't pass the wrong scalar type to a function that expects a different one, BECAUSE TS shows an error, not because it does show an error.
This is the only practical and recommened (as in used by TS, for example here https://github.com/microsoft/TypeScript/blob/99878128f032786bd3ad1295402a04ca7002eeb2/src/compiler/builder.ts#L1073) to subclass primitive types. It is also used to subclass object types, but this is less of an issue as objects can have excess properties in any case.
One possible solution to this would be to allow users to exclude the brand field names from typia typechecking, so for example I could use:
typia.assert<Weeks>(x, { omit: ['__brand']}); // Check x is a Weeks, ignoring the brand field at runtime
This may be a less than optimal syntax, but the implementation would be to simply not generate type checks for the specified fields, ignoring them all together since they are ONLY referenced at compile time and never exist at runtime.
How long do we have to repeat the same story?
This is runtime validator libary, so that no way to distinguish the brand type. If string & object
type comes, how to detemine which one is valid and the other is not? Also, considering the object & object
brand type case, can you make an algorithm which one to keep?
If you still want the brand type, consider to use custom tag.
Otherwise not, don't try to compel typia
to adapt the nonsensible case. You're even forcibly casting by as
statement, isn't it? Such anti-pattern like hard-casting is not suitable for validator feature.
I understand your story. However, you've not answered the point:
Your current argument is "it's not possible as it's not compilable typescript", which ignores the fact it's both common and useful.
The link I provided is the canonical case - the type is a number, but has a member that never exists at runtime, however it is asserted at compile time to prevent assignments like "aWeek = aDay" - both variables are numbers so this is syntactically possible, but we choose to highlight this from the type systems as an error.
Again, the solution is to use some marker that the brand field should be checked at runtime. The use case is similar to the typia.tags.Type<"uint32">
intersection. In this case a typia.tags.NoRuntimeCheck
would be ideal, as in:
export type Brand<K, T> = K & { __brand: T & typia.tags.NoRuntimeCheck };
In this way, we're permitting the TS language server to enforce the type correctness, but asserting that the __brand
field should not be checked at runtime.
Although I understand very well your irritation (I've worked on a few open source projects!), the use case is clear, and I've suggested some approaches to how it could work.
Since you clearly don't have the time to look into this yourself, perhaps you could point me at the correct place in the repo where I might implement typia.tags.NoRuntimeCheck
and accept a PR?
Another way of looking at it is the typia.tags.NoRuntimeCheck
is like the optional type marker you suggested, but ONLY at runtime, not dev/compile time
No matter how much you claim that "this is reasonable", in reality it is not.
The Brand type is contradictory and is an area that validators cannot support. When considering type spec, you have to consider the aspects of the TypeScript, "Checks the type as a set-inclusion relationship". You're not considering the set-inclusion relationship, and just saying "this is possible" based on your intuition.
Also, I don't have plan to adapt anti-pattern specification like Brand & tags.NoRuntimeCheck
type.
Dear Jeongho Nam, I have great respect for your technical capabilities. This is your project, and you should do as you see fit.
But your comment No matter how much you claim that "this is reasonable", in reality it is not.
suggests you haven't considered many common use cases, or the purpose of TypeScript: to allow a wide range of typical errors to be determined at dev/compile time, instead of runtime.
Many typed languages support the concept of subclassed primitives. In Typescript, template types, keyof index types and types such as 'week'
are all subclasses of string
. In B (the precursor to C), pointers and integers were originally unified machine words, but became specialised in C (tho retaining some arithmetic operators). The syntax developed to help programmers avoid errors, but the implementation remains to this day as a "primtive" machine word.
Do you have an alternative proposal to allow subclassing of primitives? How would I create a type in TS that has all the properties of a number but can only be assigned/passed to a compatible type?
In your example
const error: BrandedNumber = 3;
...you have misunderstood the use case. The correct usage would be:
const error = 3 as BrandedNumber; // or <BrandedBumber>3
... the purpose is to FORCE the developer to specify what type of number we are considering, in the same way that many declarations require a type in TypeScript, notably if they can't be inferred.
The mechanism for this is definitely imperfect (since TS does not have specific syntax for this), but not only does it work, it is used widely demonstrating it's utility.
In JS, there are also real examples such as number & AsyncIterator<number>
that are perfectly sensible types, since they actually used boxed types (as in let x = Object.assign(123,{ [Symbol.asyncIterator]() { ... } }
). These should, of course, work as runtime as the fields actually exist, however it also fails in Typia.
(edited: wrong screenshot)
The assignment const x = Object.assign(123,{ u: 'u'});
is both valid Javascript and TypeScript, generating the correct type:
const x: 123 & {
u: string;
}
...indicating that TS can indeed model such types, but Typia cannot generate a validator.
Rather this dismiss this class of problems - types which are correctly modelled in TypeScript but for which Typia cannot currently validate - could you explain your position that the type number & {...}
is "nonsensical" when it is both executable JS and correctly modelled TS?
I ask this out of respect - I'm not trying to troll you! The second case above has nothing to do with branding, but is simply a TS type that can't be validated currently.
Looking at the function iterate_metadata_intersection
, validating const x = Object.assign(123 as number /* not const */,{ u: 'u'});
above as:
console.log(typia.validate<Number & { u: string }>(x))
...works as expected (in a general sense - I've not confirmed the constant case).
Perhaps the solution to validating this type of intersection is to validate: primitive & object
as Primitive & object
?
If you still want the brand type, how about determining a policy for the priorities between each element that composing the intersection type, especially about the nonsensible case.
Note that, the rule must not be ended by showing just simple intersection type like number & BrandType
case and just saying "It is possible, it is simple". The policy must be possible to cover extreme situtation like combination of complicate intersection and union type case.
(A | B | C) & (D | E) & (F | G)
(((A | B | C) & (D | E) & (F | G)) & X) | ((a | b) & (c & d) & x)
I appreciate the extra insight. Perhaps we should ignore the "brand" case for now, and consider the Object.assign(num,{ ...})
case, since that generates the correct type within TypeScript. This does seem to be specifically about intersections of primitives with objects, in that objects can (of course) be intersected with themselves.
The difficulty comes from the fact, I guess, that the JS Object.assign(123,null)
actually boxes the 123, as if you had typed new Number(123)
, but TypeScript claims it is number
.
Given that boxed objects only have a single prototype chain, they can't be more than one primitive type. The intersection number & string
is indeed nonsensible as you can't have a single identfier that has two different primitive prototypes
This would reduce the available cases to primitive & object
, not primitiveA & primitiveB & object
In summary:
import typia from "typia";
let n = 123;
const x = Object.assign(n,{ u: 'u' }); // TS type: `const x: number & { u: string; }`
console.log(typia.validate<Number & { u: string }>(x)); // Works
// console.log(typia.validate<typeof x>(x)); - nonsensible
To fix the above example (not "branding", but auto-boxing), the issue can be resolved by correctly (or rather, more strictly) implementing the ObjectConstructor. The following code runs without error:
import typia from "typia";
type Boxed<T> =
T extends number ? Number :
T extends string ? String :
T extends boolean ? Boolean :
T extends bigint ? BigInt :
T;
declare global {
interface ObjectConstructor {
assign<T extends {}, U>(target: T, source: U): Boxed<T> & U;
new <T>(value?: T): Boxed<T>;
<T>(value: T): Boxed<T>;
}
}
let n = 123;
const x = Object.assign(n,{ u: 'u' });
console.log(typia.validate<typeof x>(x))
const nn = Object(456);
console.log(typia.validate<typeof nn>(nn));
And what is the problem with making the brand field optional? Everything will work exactly the same way.
I suggest making a special tag for this.
@AlexRMU - how do you mean "optional"?
type Weeks = number & { _brand: 'Week' }; // Fails in typia as "nonsensible"
type Weeks = number & { _brand?: 'Week' }; // Works in typia, but does not fail type-checking during assignment without a type assertion, which is the whole point of the technique
...the only "failure" case is const b = 123;
- this can now be passed to anything that expects a branded number irrespective of the brand, because the "brand" is optional.
My personal view is a typia.tag.NoRuntimeCheck
would be ideal as it specifies that a member could be required at compile time, but absent at run-time. This is a rare edge case, as far as I know only useful to this use-case, but it logically solves the problem in a very easy to reason about way. @samchon doesn't like a run time validator that can optionally not validate, which I completely get, but it doesn't help solve this problem, which is used in codebases I'm responsible for extensively.
For anyone interested, I implemented typia.tag.NoRuntimeCheck<T>
to make by brands work in my fork at https://github.com/MatAtBread/typia/pull/1
See the notes on installation and the example:
import typia from "typia";
type B = number & { _brand: typia.tags.NoRuntimeCheck<'b'> };
type C = number & { _brand: typia.tags.NoRuntimeCheck<'c'> };
function x(a: number) {}
function y(a: B) {}
function z(a: C) {}
const b = 123 as B;
x(b); // Correct: b is a number
y(b); // Correct: b is a B
// @ts-expect-error
z(b); // Error (Correct!): b is not a C
console.log("Branding 1:",b * 10, typia.validate<B>(b)); // Correct: 1230
This works because an intersection between a single primitive and an "empty" object isn't 'nonsensible' in the current typia implementation. This tag marks the field for exclusion in the same test.
I've not submitted a PR to @samchon, as he's previously indicated he doesn't think "branding" is a reasonable TS use-case.
@MatAtBread Yes, I still think that the reinterpret casting by as
statement seems not valid.
If I needed the discrimination, I just utilzed actual brand type, and it is much type safer.
// UNSAFE
type Cat = string & { tag: "cat" };
type Dog = string & { tag: "dog" };
const cat: Cat = "something" as Cat;
// TYPE SAFE
type Cat = { name: string; kind: "cat" };
type Dog = { name: string; kind: "dog" };
const cat: Cat = { name: "kitty", kind: "cat" };
Whether I like the brand type or not, you are going to support brand type, you need to find a better solution. Looking at your code MatAtBread/typia#1, NoRuntimeCheck
is not a fundamental solution to the brand type what you want, but just avoiding the confliction with current typia
system with hard coding.
To resolve this problem clearly, you have to design the priority rule. For example, when number & object (brand)
type comes, you can make a rule that number
takes precedence over object
. Or you can design another rule that left side first and right side later
. Such rule must be valid even when complicate intersection and union type comes, and there must not be any nonsensible case.
I repeat the previous example pseudo code again:
(A | B | C) & (D | E) & (F | G)
(((A | B | C) & (D | E) & (F | G)) & X) | ((a | b) & (c & d) & x)
The reason for the "branding", and not the type-safe solution you propose (which I accept is better), is because much of TS is about modelling what JS already does, not re-writing someone else's code. If a JS function in some 3rd party library takes a number as (for example) the number of Milliseconds, and another returns an RGB pixel as a number, saying:
timeSinceLastPaint(getPixel(0,0));
...looks typesafe, as getPixel(...)
returns a number and timeSinceLastPaint
expects number milliseconds, but it's clearly nonsense.
The preferred way to avoid this in TS is to "extend" number to be more specific, and currently the best way to do that is via the "brand" trick:
function timeSinceLastMillis(t: number & { _brand: 'milliseconds'}): void {}
function getPixel(x: number, y: number): number & { _brand: 'RGB565' } {}
When the code is in a module, or otherwise not part of our codebase, the "branding" pattern is a very useful way to say to the developer: "if you really want to do this, force the type". Without it, all numbers are just "numbers", which is actually rarely the case.
I completely accept that mine is the "trivial" solution, and I tried to come up with an implementation that was as minimal as possible so as not to have to understand all the clever stuff! I got quite into the typia codebase, but it's very clever ☺! I'm sure you could implement it in a better way if you had the inclination.
My solution rests on the fact that you do already permit the intersection of a primitive and an empty object. The tweak was to extend the definition of optional
in this case only to fields tagged with NoRuntimeCheck
. Otherwise, the logic is unchanged, so your pseudo code examples should work as before, as the test for a object type with no properties is only thing to have changed, not the walk down the type hierarchy.
What do you think about this? It's not a complete solution, but it works right now.
In our project we used branded nominal types to assert format requirements
Something like:
// 0..255 regex is [0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]
// 0..31 regex is [0-9]|[12][0-9]|3[012]
// CIDR v4 is [0..255].[0..255].[0..255].[0..255]/[0..32]
/**
* @pattern ^([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3}/([0-9]|[12][0-9]|3[012])$
* @minLength 12
* @maxLength 21
* @example "86.255.0.199/24"
*/
export type CidrV4 = Nominal<string, "CidrV4">;
Nominal looks like this:
export declare class Tagged<N extends string> {
protected _nominal_: N;
}
// The extra parameter "E" is for creating basic inheritance
export type Nominal<T, N extends string, E extends T & Tagged<string> = T & Tagged<N>> = (T & Tagged<N>) | E;
We use this with our current runtime validator to generate checks such that both the requested type and all patterns match. This means that the guarantees provided by runtime checking can be communicated to the type system.
For example:
function checkIpInCidrBlock(ip: IPv4, cidr: CidrV4): boolean {
...
}
const testCidr = "192.168.1.0/24"
// This will fail since the string type is too broad
checkIpInCidrBlock(testCidr)
// This narrows the string type to the nominal one
assertValid<CidrV4>(testCidr)
checkIpInCidrBlock(testCidr);
Missing support for this was one of our sticking points when evaluating typia, as this is a pattern we use a lot. Nominal types really shine when you have runtime checks to back them up.
Question
Hi all, I'm trying to migrate from class-validator/class-transformer to typia and nestia, but I'm facing some issue that don't know how yo fix, or wether is expected or not.
I have an auxiliar type to create "kinf of" nominal typing
In my DTO i use it like this
when I remove old anotations and try to use typia and nestia I'm seeing the following error (when running a typescript check)
I found this PR that seems that this should be supported.
any clue why this is happening ?
thanks