samchon / typia

Super-fast/easy runtime validators and serializers via transformation
https://typia.io/
MIT License
4.48k stars 156 forks source link

Nominal typing support (nonsensible intersection error) #911

Open sergio-milu opened 9 months ago

sergio-milu commented 9 months ago

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

declare type Nominal<Type, Identifier> = Type & {
  readonly [__nominal__type]: Identifier;
};

export type UserID = Nominal<string, 'UserID'>;

In my DTO i use it like this

export interface TestDTO {
  @ApiProperty({ type: String, format: 'uuid' })
  @IsUUID()
  userId: userId;
}

when I remove old anotations and try to use typia and nestia I'm seeing the following error (when running a typescript check)

 error TS(@nestia.core.TypedBody): unsupported type detected
- CreateRecommendationDTO.memberId: string & __type
  - nonsensible intersection

I found this PR that seems that this should be supported.

any clue why this is happening ?

thanks

samchon commented 8 months ago

What the __nominal__type type is?

sergio-milu commented 8 months ago

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

samchon commented 8 months ago

It seems actually nonsense type. Is there any reason why adding the __nominal_type__ in the DTO?

Is it required for client develoers?

sergio-milu commented 8 months ago

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

samchon commented 8 months ago

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.

sergio-milu commented 8 months ago

I changed my 'branded' types to this

export enum TestType {
  __brand = 'Test',
}

export type TestID = string & TestType;

and seems that this works, thanks!

sergio-milu commented 8 months ago

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

samchon commented 8 months ago

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.

sergio-milu commented 8 months ago

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?

samchon commented 8 months ago

I think it would better to change your type. Your case seems too domestic and occurs hard coding.

sergio-milu commented 8 months ago

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

samchon commented 8 months ago
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;
sergio-milu commented 8 months ago
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;

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

samchon commented 8 months ago

Then make the brand type's property to be optional.

typia cannot support that TypeScript compiler occurs error case.

samchon commented 8 months ago

image

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.

bradleat commented 4 months ago

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.

bradleat commented 4 months ago

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.

matAtWork commented 1 month ago

@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.

samchon commented 1 month ago

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.

matAtWork commented 1 month ago

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?

matAtWork commented 1 month ago

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

samchon commented 1 month ago
image image

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.

matAtWork commented 1 month ago

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.

And it is valid TypeScript: https://typia.io/playground/?script=JYWwDg9gTgLgBAIhgTzMAhgg3AKBysAUzgCE4BeOAOwFcQAjQqOAMjgG84B9eqdKgCYAuOAHJ6ouAF9cBYgGEK1Oo2ZtOPPoJGiAxpJl4AZjSq6YwCFTgAPABToRtBkwCUHKThNmLVuMgcREnd2T29zS2sAL0C4eRDPHF0rAGd4eiUARgAmAGY4dBTSXBx7elcsOAB6KrjoKEJzEQzgIvRlFygcAPLKmrqoBqa4FrbSHBje6tqAUUHoODt5esaYAEJXZrhW6gh4dsUcIA

Screenshot 2024-08-08 at 10 45 49

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.

This is demonstrated here: https://typia.io/playground/?script=JYWwDg9gTgLgBAIhgTzMAhgg3AKBysAUzgCE4BeOAOwFcQAjQqOAMjgG84B9eqdKgCYAuOAHJ6ouAF9cBYgGEK1Oo2ZtOPPoJGiAxpJl4AZjSq6YwCFTgAPABToRtBkwCUHKThNmLVuMgcREnd2T29zS2sAL0C4eRDPHF0rAGd4eiUARgAmAGY4dBTSXBx7elcsOAB6KrjoKEJzEQzgIvRlFygcAPLKmrqoBqa4FrbSHBje6tqAUUHoODt5esaYAEJXZrhW6gh4dsU8ZKoUiAAbQgA6M4gAczsMgCo4TIAGCumBoZgRHNzXoA

Screenshot 2024-08-08 at 10 31 14

(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.

matAtWork commented 1 month ago

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?

samchon commented 1 month ago

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.

matAtWork commented 1 month ago

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.

matAtWork commented 1 month ago

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

matAtWork commented 1 month ago

This would reduce the available cases to primitive & object, not primitiveA & primitiveB & object

matAtWork commented 1 month ago

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
matAtWork commented 1 month ago

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));

https://typia.io/playground/?script=JYWwDg9gTgLgBDAnmYBDOAzKERwERIqp4DcAUGYQKZwBCEAHlQCYA8AKgHxwC8ZcAuOzhUGMKgDtmAZzgSAriABGVKHAD8cAHKKVagFz9Bw0eKmzpMKMAkBzDXADKVm-cOChIsZJlwlECAAbKlQJB3ogkLD3Yy8zXyVgWxt4TVokgEkJeBiBdnIyZioAY0DUKBpbQIglVEC4AG8jARTVDFRimgB5JQArEpgAYQgJSyh5YphoRuaPOFRpaSSJDjifWQaAXwAaOABVTgAKGHLbKhh9IV3pCHkoTsu9gEpL+iY2LjgAMn3yOcEJFQAO5wDhHABudXkVHUl3YLzojBYYL+-1BXEOkMC0LhCLeyK4qP+mzIJLIwXgYR4cAAjAAmADM5GKI0scAYvDgPX6kwAdAslrYJIcJNsGnB5JcAOTyKVwTZPZmsyK86q2Y7INC8rHAZiocSsagQDDso4MJ5PCgs0aUqlcvoDQ4AFgArAA2RVka03YKqiDqwhanV6g1Gk0SCRHCMWkhAA

AlexRMU commented 1 month ago

And what is the problem with making the brand field optional? Everything will work exactly the same way.

AlexRMU commented 1 month ago

I suggest making a special tag for this.

matAtWork commented 1 month ago

@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

This is only partially true. See https://typia.io/playground/?script=JYWwDg9gTgLgBDAnmYBDOAzKERwERIqp4DcAUGYQKZwBCcAvHAHYCuIARlVHAGRwBvOAH0OUVMwAmAfgBccAOQcFcAL7lqcAMKMW7Lj35DR4qXMUBjFeooZWzCzGARmcAB4AKVPLaduASkFVMjsHJxc4RC95WkCBYNDHZ1cAL2jtOOCyATI4OAsXAGd4Dl0ARgAmAGZyXPcPDn8SODqoxua6tPa4AHoeuAB5CDBCgEI4ADkIOAB3VChmYGYAczIsnLyC5mK4Dll6JkqaijzPdtaGps7L5r64AFEobB4PLWgoKkdR-3lS4EKWBB4OgdGsKBt8kUSuVqnBUADaLVTjdev03k9PjBfnB-nC9H4oBdund0R9HNjcehaNdif1Hs84K93pjvhSAcwgXjQVkgA

...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.

MatAtBread commented 1 month ago

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.

samchon commented 1 month ago

@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:

MatAtBread commented 1 month ago

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.

AlexRMU commented 1 month ago

What do you think about this? It's not a complete solution, but it works right now.

jfrconley commented 4 weeks ago

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.