-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Branded types #3
Comments
Hey @vriad, I actually made an internal library where we had to solve for this because we validate large nested objects. I've wrote up some stuff that may be of use https://github.com/ryansmith94/rulr/blob/v6/readme.md |
My "option 2" is misguided; it doesn't actually achieve the same thing as true branded/opaque types. @krzkaczor |
You can do opaque types with intersections upon any types - and can be very useful if you use symbols throughout. The way they show there is less useful as it can't be saved and queried (you cant add multiple "brands" or "flags" dynamically for the type system to later use to infer from). For example, I am playing right now with seeing if I can build out a syntax that would still type statically just for fun, doubting I'd actually try to go past typing it all out -- but this is what i came up with, which basically utilizes opaque typing to achieve it: const one = { one: wrap.optional.string(), four: 3 } as const;
const two = { two: 2 } as const;
const validator = {
foo: wrap.optional.string(),
bar: wrap.nullable.optional.literal('bar'),
baz: 123,
qux: 'qux',
blah: wrap.unequal(2),
union: wrap.optional.nullable.union([wrap.string(), wrap.number()]),
intersect: wrap.intersection([one, two]),
key: wrap.forbidden(),
undef: wrap.undefined(),
} as const; Notice that it is simply property accessing - which actually does not need to be done in any given order.
type CheckNullable<
T extends AnyObj,
V
> = T['__nullable'] extends typeof NULLABLE ? null | V : V;
type CheckOptional<
T extends AnyObj,
V
> = T['__optional'] extends typeof OPTIONAL ? undefined | V : V;
type CheckFlags<T extends AnyObj, V> = V extends infer O
? CheckNullable<T, O> | CheckOptional<T, O>
: never;
type TypedPrototype<T extends AnyObj> = Readonly<{
assert(value: unknown): asserts value is T;
guard(value: unknown): value is T;
union<V extends unknown[]>(arr: V): CheckFlags<T, V[number]>;
intersection<A extends unknown[] | readonly unknown[]>(
arr: A,
): IntersectionOf<A[number]>;
optional: TypedPrototype<T & { readonly __optional: typeof OPTIONAL }>;
nullable: TypedPrototype<T & { readonly __nullable: typeof NULLABLE }>;
negative: TypedPrototype<T>;
nonnegative: TypedPrototype<T>;
positive: TypedPrototype<T>;
nonpositive: TypedPrototype<T>;
string<V extends string>(): CheckFlags<T, V>;
number<V extends number>(): CheckFlags<T, V>;
bigint<V extends bigint>(): CheckFlags<T, V>;
regexp<V extends RegExp>(): CheckFlags<T, V>;
symbol<V extends symbol>(): CheckFlags<T, V>;
primitive<V extends Primitive>(): CheckFlags<T, V>;
unknown<V extends unknown>(): CheckFlags<T, V>;
undefined(): undefined;
forbidden(): never;
any<A extends any>(): A;
literal<V>(value: V): CheckFlags<T, V>;
equal<V>(value: V): V;
unequal<R, V = unknown>(value: V): R;
}>; which has a schema like: but finally infers into: All that being said, I am not sure I personally see that much use in having this library provided branded types. I think they just sound like they'd be cool but in practice have very little actual use. The larger issue usally becomes once they are branded they no longer pass as their wider type, so it makes your entire application insanely complex trying to manage all the cases and potential brands things may have. A brand may be useful for the top-level object which indicates that it has been validated, however -- and that can even work at runtime. For example, you could return a new object when const obj = {
positive: 2,
foo: 'one',
bar: 'bar',
baz: 123,
qux: 'qux',
blah: 3,
union: 2,
intersect: {
one: 'hi',
four: 3,
two: 2,
},
} as const;
const validatedSchema = schema.validate(obj);
function example(o: Inferred & typeof VALIDATED) {
console.log('Validated Only Can Call');
}
validatedSchema.union;
example(validatedSchema);
example(obj); // <--- not assignable to typeof VALIDATED |
@colinhacks what did you decide regarding branded type? I've just attempted migrating from Runtypes because I really want strict/excess property check (which Runtypes lacks) but I'm stuck on branded types. We use them mainly for simple types like UnsignedInteger, JsonDate etc. that are respectively const zJsonDate = z.string().refine((str) => isValid(parseJSON(str))); With branding, we "know" down the line that they have been checked. Without them, we have to recheck locally and then cast to the branded type, but then it defeats the purpose of the initial check, we might as well just expect string for JsonDate for example and then have: type JsonDate = z.infer<typeof zJsonDate> & {__jsonDateBrand: true};
function checkJsonDate(val: string) {
return zJsonDate.parse(val) as JsonDate;
}
Might be worth checking the Runtypes implementation, I'm not an expert but it didn't look too convoluted: |
@jraoult someone provided an example of how to implemented branded types on top of Zod here, might be useful for you. #210 (comment) |
@colinhacks great thx for pinging me! I'll try that. |
Hi @colinhacks, is the support for ReadOnly types planned? |
@maneetgoyal this approach should work: #210 (comment) |
This is the recommended way to do tagged/branded types in TypeScript/Zod: type Tagged<T, Tag> = T & { __tag: Tag };
type UUID = Tagged<string, 'UUID'>;
const uuid: z.Schema<UUID> = z.string().uuid() as any;
uuid.safeParse('ae6cd9c2-f2e0-43c5-919c-0640b719aacf'); // Success, data has type UUID
uuid.safeParse(5); // Fail
uuid.safeParse('foo'); // Fail |
Is there any way without using any? |
@danielo515 did you happen to find a type safe workaround? |
There is now a native way for this https://github.com/colinhacks/zod#brand |
Feature request: branded types
This issue can be the primary discussion ground for implemented branded types.
Option 1:
Replicating io-ts's symbol trickery to create types like NonZeroNumber or Email (it might be difficult to do that one while remaining ergonomic)
I don't love all the boilerplate associated with
io-ts
branded types (i.e.interface PositiveBrand { readonly Positive: unique symbol }
).Option 2
It may be able to include string/number validators as a literal generic argument of the ZodString/ZodNumber classes, like so:
That way the validations being enforced are easy to see with a glance at the type definition. This would only work for built-in validators I believe (?). This is also different, in that validations are registered at the instance level instead of the class level.
The text was updated successfully, but these errors were encountered: