-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
.optional() and --exactOptionalPropertyTypes #635
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
Comments
I soo want to start using |
I wonder if we can introduce this under a separate method like |
If we compare with io-ts, where everything seems to work with To add undefined as part of a property, you use
To make properties partial (i.e. with question mark), you use
This leads to a very strict handling of undefined and question marks, which goes very well along with It is important to note that when you use In Zod, we have Then we have The addition of the question marks is done automatically by this code inside
So the issue is that the question mark handling is currently inherently tied to whether the property type extends |
This might be drastically important when you work with document based databases, where a property might be a Dict of ID => VALUE type ProductId = string & {readonly _: unique symbol}
type Entry = {id: ProductId; status: string}
type ProductsA = {[k in ProductId]: Entry}
declare const products: ProductsA
declare const productId: ProductId
// productA: Entry -> BAD, because products[productId] might be undefined
const productA = products[productId]
// Another approach, optional property
type ProductsB = {[k in ProductId]?: Entry}
declare const productsB: ProductsB
// product: Entry | undefined -> GOOD
const productB = productsB[productId]
// However, now we can:
productsB[productId] = undefined
// because without "exactOptionalPropertyTypes" enabled, {[k in ProductId]?: Entry} -> {[k in ProductId]?: Entry | undefined}
// also, given toArray conversion of a record:
type RecordValues<T> = T extends {[k: string | number | symbol]: infer V}
? V[]
: never
// we get correctly, but unintentionally (Entry | undefined)[]
declare const arrayOfProductsB: RecordValues<ProductsB>
// "exactOptionalPropertyTypes" enabled behavior: don't append "undefined" to optional propertie's value type |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
I don't think this issue should be considered stale. It is a real issue that multiple users of the library are facing. |
Any progress on this ? Would you accept a pull request to add an |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
Still relevant |
I don't think this should be closed due to staleness. This issue is currently blocking me from adopting |
Relevant issue just silently closed, regardless of activity, no word from author. Later can be understood if he has no time, but let bots close issues regardless of relevance and activity feels wrong. |
Another issue with this function dynamicZodObject<T>(t: T) {
return z.object({
arbitrary: z.any().refine((x): x is T => t),
});
} I spent so much time trying to fix it properly in zod to make a PR, but seems like it's just impossible. Right now i am fixing it like this export type ZodObjectIdentity<T extends z.ZodRawShape, Side extends '_input' | '_output'> = {
[K in keyof T]: T[K][Side];
};
export function zodObjectIdentity<T extends z.ZodRawShape>(
t: T,
): z.ZodObject<T, 'strip', z.ZodTypeAny, ZodObjectIdentity<T, '_output'>, ZodObjectIdentity<T, '_input'>> {
// @ts-ignore
return z.object(t);
} So, despite the |
(Moving my complaints in #1540 and #1510 here, because it sounds like the same issue.) My specific problem is that I'm trying to parse values into this type: type FormattedText = { text: string; bold?: true; }; The // Valid values include
{ text: "foo" }
{ text: "foo", bold: true }
// Invalid values include
{ text: "foo", bold: false }
{ text: "foo", bold: undefined } This schema is designed to work with Slate, and the values are in a database. The design is therefore non-negotiable. I can't figure out how to configure zod to make the I realize I could do this with This issue is blocking me from using zod, because I can't find any reasonable workaround for it. I understand that zod is designed around an assumed TypeScript config that its users are supposed to set. This makes sense: you can't design for all But is it documented somewhere what TypeScript config zod is designed for, and why? As a design principle, I would design zod around the "strictest" form of TypeScript, i.e. for users that are using TypeScript effectively. So is there a particular reason that zod is designed for users with |
@jameshfisher Well, See this in TS playground and toggle // exactOptionalPropertyTypes always ON
type UserIdBrand = {readonly UserId: unique symbol}
type UserId = string & UserIdBrand
type User = {
id: UserId
name: string
}
declare const id: UserId
declare const r1: Record<UserId, User>
declare const r2: {[_ in UserId]: User}
// noUncheckedIndexedAccess "1" => u1 & u2: User | undefined (bad)
// noUncheckedIndexedAccess "0" => u1 & u2: User (good)
const u1 = r1[id]
const u2 = r2[id]
declare const a1: User[]
declare const t1: [User, User]
// noUncheckedIndexedAccess "1" => u3: User | undefined (good), u4: User (good)
// noUncheckedIndexedAccess "0" => u3: User (bad), u4: User (good)
const u3 = a1[0]
const u4 = t1[0] What So we have this two features that didn't made it cleanly into the With However, the changes made in zod have not improved, but worsen the situation as I've explained already. This is why I think we need a specific API with type and runtime behavior guaranteed.
The 2. will however have the |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
Still relevant |
this worked for me: #2314 (comment) EDIT: Nvm, after using the .parse() method it makes the validated data's properties back to |
Still relevant |
So many reactions! 🔥 This patch can be installed for testing by adding the following lines to "zod": "npm:@bazuka5801/[email protected]" |
@bazuka5801 thanks, this is very helpful! Would be great to see this approved & merged... |
Note this has actual runtime implications, e.g. This is currently impacting me too. Also something to consider (I'm not sure if this was brought up above) is whether... z.object({ a: z.string().exactOptional() }).safeParse({ a: undefined })
z.object({ a: z.string() }).exactPartial().safeParse({ a: undefined }) ...should fail to parse or instead parse successfully but omit the key (like JSON serialization behavior), i.e. a way to affect both EDIT: I think my last paragraph can be simulated like this. type OmitUndefined<Object, Keys> = {
[K in keyof Object]: K extends Keys
? Exclude<Object[K], undefined>
: Object[K];
};
/**
* Zod's `optional`, `partial`, etc. generate a type of `{ key?: SomeType | undefined }` but, since
* we are using `exactOptionalPropertyTypes` in tsconfig, we want `key?: SomeType`.
*
* This function takes an object and removes the `undefined` from some optional properties.
*
* See https://github.com/colinhacks/zod/issues/635
*/
export function transformOmitPartial<
Object extends object,
Keys extends keyof Object,
>(keysToOmit: Array<Keys>): (value: Object) => OmitUndefined<Object, Keys>;
export function transformOmitPartial<Object extends object>(): (
value: Object,
) => OmitUndefined<Object, keyof Object>;
export function transformOmitPartial<
Object extends object,
Keys extends keyof Object,
>(keysToOmit?: Array<Keys>): (value: Object) => OmitUndefined<Object, Keys> {
return (value: Object) => {
const realKeysToOmit =
keysToOmit ?? (Object.keys(value) as Array<keyof Object>);
for (const key of realKeysToOmit) {
if (key in value && typeof value[key] === undefined) {
delete value[key];
}
}
return value as OmitUndefined<Object, Keys>;
};
} Not great because after |
fwiw I would happily suffer through a major version bump to get rid of the optional key magic for |
API-wise, rather than adding some new type, this should be behavior you can configure when initializing zod, e.g. |
i just want to call out that there is a footgun for consumers of zod types that needs to be avoided: let's say i have a type for email recipients, consisting of the email address and an optional name. interface EmailRecipient {
email: string;
name: string | undefined;
} when users try to pass just an email, they are prompted by typescript to either pass a name or explicitly pass undefined. great. when zod renders the type with a question mark, interface EmailRecipient {
email: string;
name?: string | undefined;
} a consumer can pass the email, typescript passes, and they have no idea that it's possible to pass a name. so there can be places littered around a codebase with inconsistent usage of the that name field. now the debugging developer has to trace every spot manually to figure out why the name isn't showing up. this is exactly the sort of problem that typescript is supposed to help us avoid. |
@benhickson Just wanted to add that javascript CAN determine the difference at runtime, as it has been said by others, stated by @alvaro-cuesta i.e. And it definitely has runtime impact for libraries which treat them differently! const foo = {a: undefined};
// undefined
const bar = {};
// undefined
console.log(foo.a)
// undefined
console.log(bar.b)
// undefined
console.log(Object.hasOwn(foo, 'a'))
// true
console.log(Object.hasOwn(bar, 'a'))
// false Then a logic as the following would be all wrong without the option to have exact optional properties (with runtime checks): // obj defines properties to be set in database
function dbInsert(obj: Partial<Document>) {
const documentInsert = {};
for (const property in obj)
{
documentInsert[property] = obj[property];
}
database.insert(documentInsert);
// error: field value cannot be undefined!
} |
Thanks @NorthBlue333 , I've edited my comment. I think that makes it even more important to fix! |
Running into this exact issue using the We're approaching 3 (!) years since this issue was first brought up, does anyone have proposals for how to resolve this? |
For anyone not having checked out all the other links: According to #2675 this is on the roadmap for ZOD 4 and a RFC will be published "soon". |
…partial(). More info: colinhacks/zod#635
I am using this hack in the meanwhile: export const mutateStrictOptionalFields = <
T extends z.ZodObject<any>,
K extends keyof T['shape'],
>(schema: T, keys: Array<K>)
: z.ZodObject<
Omit<T['shape'], K> & { [k in K]? : T['shape'][k]} ,
T['_def']['unknownKeys'] ,
T['_def']['catchall'] ,
{
[k in keyof T['shape'] as Exclude<k, K>]: T['shape'][k]['_output'];
} & {
[k in K]?: T['shape'][k]['_output'];
},
{
[k in keyof T['shape'] as Exclude<k, K>]: T['shape'][k]['_input'];
} & {
[k in K]?: T['shape'][k]['_input'];
}
> => {
for (const key of keys) {
schema.shape[key] = schema.shape[key].optional() ;
}
// @ts-expect-error
return schema.refine(arg => keys.every(key => !(key in arg) || arg[key] !== undefined)) ;
} I do not know how reliable it is, but it passes tests + type-checks so far. |
PR was closed without merge, there doesn't seem to be a proper solution for this issue yet |
my current workaround is enforcing a |
I honestly never thought about it this way... thank you for the aha moment, on a Friday nonetheless! |
Currently, .optional() behaves like:
This results in a type mismatch when you're using --exactOptionalPropertyTypes in TypeScript 4.4 and expecting type
C
to match an interface definition like:.partial(), .partialBy(), .deepPartial() all have the same issue.
It would be nice to unbundle the optionality of the key from the union type with undefined for the value.
I suggest that these methods be changed to specify the optional absence of the key by default, and perhaps accept an option to restore the old behavior of adding
.or(z.undefined())
to the value schema(s). This would unfortunately be a breaking change, but it makes more sense than the current behavior, especially as more projects adopt --exactOptionalPropertyTypes.The text was updated successfully, but these errors were encountered: