nestia icon indicating copy to clipboard operation
nestia copied to clipboard

Branded types support

Open rivatove opened this issue 1 year ago • 2 comments

Feature Request

Nestia throws the following error on branded properties:

- number & Brand<"ProductPrice">:
- nonsensible intersection

With the ProductPrice being of the following type (with a standard Branded implementation):

type ProductPrice = Branded<number, 'ProductPrice'>;

I wonder if there is a way for Nestia to reduce the above type to just number?

Maybe we could somehow define the Brand wrapper to make Nestia ignore intersections with that type? Or to define the internal brand property key (in my case __brand) to ignore?

rivatove avatar Oct 13 '24 12:10 rivatove

Branded type is not supported, because it needs re-interpret type casting, the type unsafe strategy.

samchon avatar Oct 13 '24 13:10 samchon

Branded type is not supported, because it needs re-interpret type casting, the type unsafe strategy.

I'm aware it requires type casting. Hence it shouldn't be enabled by default.

But branded types are widely used. So an escape hatch is useful because they're gonna be used anyway. And having a built in way to do that increases Nestia's appeal.

I dont see what would be the harm in defining an object key in the config, which if an object has, and the object is a part of an intersection, makes the object get discarded from the intersection by Nestia.

It's a completely opt in behavior and the user can be made aware of the risks if the key is mistyped or not unique.

rivatove avatar Oct 13 '24 13:10 rivatove

Close by same reason with https://github.com/samchon/typia/issues/933.

I am not affordable to develop the feature, so give up this spec or do contribute please.

samchon avatar Dec 02 '24 18:12 samchon

If you stumbled across this issue, I have patched typia to support brands. See https://github.com/samchon/typia/issues/911#issuecomment-2515870176

MatAtBread avatar Dec 04 '24 00:12 MatAtBread

If you stumbled across this issue, I have patched typia to support brands. See samchon/typia#911 (comment)

This problem must be solved by defining priority relationship when insensible intersection type case.

samchon avatar Dec 04 '24 01:12 samchon

@samchon idk why you insist on fighting your community on this. you've created a great tool and @MatAtBread has a reasonable solution to everyone's problem. please adopt it.

bradleat avatar Aug 08 '25 22:08 bradleat

There is an incredibly simple userland solution for this. I'm not sure if it covers all cases, but it worked flawlessly for me for object validation.

You just have to unwrap the branded types before passing it to typia.

import { UnwrapTagged } from "type-fest";
import { Tag } from "type-fest/source/tagged";
import { Brand } from "effect";


// Recursively unwrap tagged types from all properties on an object
// Functions are ignored and excluded

// With type-fest's Tagged
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type UnwrapTaggedDeep<T> = T extends Tag<PropertyKey, any>
  ? UnwrapTaggedDeep<UnwrapTagged<T>>
  : T extends (infer U)[]
  ? UnwrapTaggedDeep<U>[]
  : T extends object
  ? {
      // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
      [K in keyof T as T[K] extends Function ? never : K]: UnwrapTaggedDeep<
        T[K]
      >;
    }
  : T;

// With Effect's Brand
export type UnbrandedDeep<T> = T extends Brand.Brand<string>
  ? UnbrandedDeep<Brand.Brand.Unbranded<T>>
  : T extends (infer U)[]
  ? UnbrandedDeep<U>[]
  : T extends object
  ? {
      // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
      [K in keyof T as T[K] extends Function ? never : K]: UnbrandedDeep<T[K]>;
    }
  : T;

And then use it like this:

if(typia.is<UnwrapTaggedDeep<MyTaggedObject>>(obj))
{
  return obj as MyTaggedObject;
}
if(typia.is<UnbrandedDeep<MyBrandedObject>>(obj))
{
  return obj as MyBrandedObject;
}

You can use assert the same way.

These rely on the unwrapper types UnwrapTagged and Brand.Unbranded provided by type-fest and Effect, but you can check their source code to make your own unwrapper if you don't have one yet.

Let me know if this solution is good enough for your use cases!

blaiseludvig avatar Sep 03 '25 18:09 blaiseludvig

@blaiseludvig -- Thanks for this! One small fix, this doesn't work with the Typia tag types, e.g. tags.Format<"uuid"> as it ends up unwrapping everything to an object. I'm not sure this is a foolproof solution, but I just ignored objects with typia.tag properties and it seems to be working:

type IsTypiaType<T> = T extends { "typia.tag"?: any } ? true : false;

export type UnwrapTaggedDeep<T> = T extends Tag<PropertyKey, any>
  ? UnwrapTaggedDeep<UnwrapTagged<T>>
  : T extends (infer U)[]
  ? UnwrapTaggedDeep<U>[]
  : T extends object
  ? IsTypiaType<T> extends true
    ? T
    : {
        // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
        [K in keyof T as T[K] extends Function ? never : K]: UnwrapTaggedDeep<T[K]>;
      }
  : T;

a-non-a-mouse avatar Oct 09 '25 12:10 a-non-a-mouse

Also, for anyone else finding this, type-fest@5 no longer exposes Tag (looking at the source it never meant to, and I don't quite understand why Typescript ever exported it, but that's an aside).

I ended up adding this to my tsconfig:

{
  "compilerOptions": {
    "paths": {
      "type-fest/source/tagged": ["./node_modules/type-fest/source/tagged.d.ts"]
    }
  }
}

And re-defining Tag locally:

import { TagContainer } from 'type-fest/source/tagged';

type Tag<Token extends PropertyKey, TagMetadata> = TagContainer<{[K in Token]: TagMetadata}>;

a-non-a-mouse avatar Oct 10 '25 10:10 a-non-a-mouse