-
-
Notifications
You must be signed in to change notification settings - Fork 163
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
Nominal typing support (nonsensible intersection error) #911
Comments
What the |
oh yes sorry, forgot to copy it
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 Is it required for client develoers? |
nop, is client-side we translate those types to just 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 Previous PR (#657) allowed only when the |
I changed my 'branded' types to this
and seems that this works, thanks! |
I've realized another issues arised with this change, and is that the validator fails
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. |
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
then we can use it the same way we use typia's validatos 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. |
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 No way for const value: number & { nested: Record<string, string> } = 3; |
dont think this is 'too domestic' this is exposed in really well-known libs 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.
|
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
They want to do
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
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:
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:
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 If you still want the brand type, consider to use custom tag.
Otherwise not, don't try to compel |
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
In this way, we're permitting the TS language server to enforce the type correctness, but asserting that the 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 |
Another way of looking at it is the |
Looking at the function
...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: |
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
|
I appreciate the extra insight. Perhaps we should ignore the "brand" case for now, and consider the The difficulty comes from the fact, I guess, that the JS |
Given that boxed objects only have a single prototype chain, they can't be more than one primitive type. The intersection |
This would reduce the available cases to |
In summary:
|
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:
|
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"?
...the only "failure" case is My personal view is a |
For anyone interested, I implemented 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 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#1, To resolve this problem clearly, you have to design the priority rule. For example, when I repeat the previous example pseudo code again:
|
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:
...looks typesafe, as 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:
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 |
What do you think about this? |
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. 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. |
Branding is valuable and it exists in other libraries (e.g. zod). At first glance, I instinctively assumed Typia was branding primitives with the tags types. Probably an incorrect assumption on my part. This doesn't contribute to the solution, but I just wanted to highlight another popular method of branding. Branding with Symbols example: // brand.ts
// We don't export the Symbol to:
// 1. Prevent other code from forging branded types by manually adding the brand property
// 2. Prevent runtime attempts to access the non-existent branded property
const brand = Symbol('brand');
export type Brand<T, B> = T & { [brand]: B }; // volume.ts
import { Brand } from './brand';
export type Volume = Brand<number, 'Volume'>;
export function isVolume(value: unknown): value is Volume {
return typeof value === 'number' && value >= 0 && value <= 11;
} // usage.ts
import { Volume, isVolume } from './volume';
let myVolume: Volume | null = null;
const next = 10;
myVolume = next; // ❌ Error: Type 'number' is not assignable to type 'Volume'
if (isVolume(next)) {
myVolume = next; // ✅ OK
} |
This comment was marked as outdated.
This comment was marked as outdated.
seems like v7 has been released 🎉 , can you point us the to the change related with this issue? thanks :) |
import typia, { tags } from "typia";
typia.protobuf.message<IMember>();
interface IMember {
id:
| (string & tags.Sequence<11>)
| (number & tags.Type<"uint64"> & tags.Sequence<12>)
| (Uint8Array & tags.Sequence<13>);
name: (string & tags.Sequence<20>) | null;
children: Array<IMember> & tags.Sequence<30>;
keywords: Map<string, string> & tags.Sequence<40>;
thumbnail:
| (string & tags.Format<"uri"> & tags.ContentMediaType<"image/*">)
| Uint8Array;
email: string & tags.Format<"email">;
hobbies: Array<IHobby>;
}
interface IHobby {
id: string & tags.Format<"uuid">;
name: string;
valid: boolean;
} Even though v7 update has succeeded to extending the tagged intersection type to array or object types, still not easy to support the nonsensible type case in current Rather than supporting a very small number of use cases that are not used often and face all kinds of logical contradictions, I choose to delay for a long time. I would like to give up on implementing this feature for at least 6 months, as support for brand types interferes with If anyone wants to contribute, change the logic in the file below and try to pass the test program. |
For those of us who's projects aren't "low important things", or "have time to waste", I have reapplied my patch to 7.0.1 Drop in install:
Usage:
The |
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
The text was updated successfully, but these errors were encountered: