Skip to content
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

Support non-structural (nominal) type matching #933

Closed
fgodino opened this issue Jan 19, 2024 · 4 comments
Closed

Support non-structural (nominal) type matching #933

fgodino opened this issue Jan 19, 2024 · 4 comments
Assignees
Labels
duplicate This issue or pull request already exists good first issue Good for newcomers invalid This doesn't seem right question Further information is requested wontfix This will not be worked on

Comments

@fgodino
Copy link

fgodino commented Jan 19, 2024

Feature Request

Hi! First of all, congratulations on this project. It's amazingly well structured.

I started using it for one of my projects where I use Nominal typing (aka branded typing). Since typescript is structural, I am mimicking it using an intersection of primitive type and object (See https://twitter.com/mattpocockuk/status/1625173884885401600?lang=es). For example:

type UserId = string & { __brand: 'UserId' };
const userId = '11' as UserId;

There are other ways though, to approach this. https://michalzalecki.com/nominal-typing-in-typescript/

The problem

The problem I am having is that when I try to use typia to compile the above example, I am getting a nonsensible intersection error.

Would it be possible to have some "special" Nominal type like typia.Nominal<T extends string | number> that allows for this kind of behavior.

Approach ideas

I am thinking the metadata constructors could identify this special case (using unique symbols as the object maybe for more safety):

declare const brand: unique symbol;
type Brand<T, TBrand extends string> = T & {
  [brand]: TBrand;
};

I think Branded types are a very common artifact in a lot of repositories and it's worth exploring it.

Let me know what do you think and if can be of any help.

Edit: I don't think this should affect validation at runtime (if the Nominal type extends primitive string, the validation should check that the input is a string).

@samchon samchon self-assigned this Jan 20, 2024
@samchon samchon added duplicate This issue or pull request already exists invalid This doesn't seem right wontfix This will not be worked on labels Jan 20, 2024
@samchon
Copy link
Owner

samchon commented Jan 20, 2024

Duplicated issue - #911

image

As you know, typia is using the pure TypeScript type directly instead of defining extra schema.

Therefore, it is not possible to support the prohibited feature in the TypeScript like this one. The branded type must not harm the TypeScript type, and have not to occur the compilation error, either. If you still want to utilize the branded type without the compilation error, make the property type to be optional. Then no problem would be happened.

import typia from "typia";

interface Branded {
  something: string & { brand?: number };
}

const branded: Branded = {
  something: "nothing",
};
typia.is(branded);

https://typia.io/playground/?script=JYWwDg9gTgLgBDAnmYBDOAzKERwERIqp4DcAUGcAHYwCmUGqAxrXAEJSpUAmt3cAbzJw4AZxy0YAC2oBzAFxiYUOXABkguACNOPAPyKqAVxBb6cAL7kLFJhCqj4Orr26KOLvnAC8g4WIlpOUU8Kgggqlk8ABoyKzJCNAA6YFEACmcePgBKcjIgA

@samchon samchon added good first issue Good for newcomers question Further information is requested labels Jan 20, 2024
@samchon samchon closed this as not planned Won't fix, can't repro, duplicate, stale Jan 22, 2024
@fgodino
Copy link
Author

fgodino commented Jan 22, 2024

I understand the issue but what about casting?

If you cast the string to BrandUser then the ts compiler works just fine

import typia from "typia";

type BrandUser = string & { brand: 'user' };

interface Branded {
  something: BrandUser
}

const branded: Branded = {
  something: "nothing" as BrandUser,
};

Besides, if I make the brand property optional then the type renders useless:

type BrandTeam = string & { brand?: 'team' };
const teamFunction = (type: BrandTeam) => {}
teamFunction('notATeam') // This should throw an error but it doesn't because of the optional property

@samchon
Copy link
Owner

samchon commented Jan 22, 2024

Don't suggest me non-standard feature violating the main principle of TS please.

If you want to enhance the type checking through the classification, just define like below. It is much safer than the brand type.

interface PasswordInput {
  type: "password";
  value: string;
}

@MatAtBread
Copy link

There's a long discussion of this and a related issue (boxing of primitives by Object(...),Object.assign(...) and new Object(...)) in #911.

I've forked and added a typia.tag to permit this usage - please read the above thread and see #911 (comment) if you wish to use "branding" with typia.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
duplicate This issue or pull request already exists good first issue Good for newcomers invalid This doesn't seem right question Further information is requested wontfix This will not be worked on
Projects
None yet
Development

No branches or pull requests

3 participants