Skip to content

Commit

Permalink
Fix extract/exclude type error
Browse files Browse the repository at this point in the history
  • Loading branch information
Colin McDonnell committed Feb 8, 2023
1 parent c8ce27e commit e71c7be
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 46 deletions.
12 changes: 8 additions & 4 deletions deno/lib/types.ts
Expand Up @@ -3648,6 +3648,8 @@ export type FilterEnum<Values, ToExclude> = Values extends []
: [Head, ...FilterEnum<Rest, ToExclude>]
: never;

export type typecast<A, T> = A extends T ? A : never;

function createZodEnum<U extends string, T extends Readonly<[U, ...U[]]>>(
values: T,
params?: RawCreateParams
Expand Down Expand Up @@ -3724,19 +3726,21 @@ export class ZodEnum<T extends [string, ...string[]]> extends ZodType<

extract<ToExtract extends readonly [T[number], ...T[number][]]>(
values: ToExtract
) {
return ZodEnum.create(values);
): ZodEnum<Writeable<ToExtract>> {
return ZodEnum.create(values) as any;
}

exclude<ToExclude extends readonly [T[number], ...T[number][]]>(
values: ToExclude
) {
): ZodEnum<
typecast<Writeable<FilterEnum<T, ToExclude[number]>>, [string, ...string[]]>
> {
return ZodEnum.create(
this.options.filter((opt) => !values.includes(opt)) as FilterEnum<
T,
ToExclude[number]
>
);
) as any;
}

static create = createZodEnum;
Expand Down
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "zod",
"version": "3.20.4",
"version": "3.20.5",
"description": "TypeScript-first schema declaration and validation library with static type inference",
"main": "./lib/index.js",
"types": "./index.d.ts",
Expand Down
78 changes: 41 additions & 37 deletions playground.ts
@@ -1,55 +1,59 @@
import { z } from "./src";
// import { z } from "./src";

const aaa = z.object({ a: z.string(), b: z.number() });
const bbb = aaa.extend({ b: z.string() });
// const arg = z.enum(["a", "b"] as const);
// const arb = arg.exclude(["b"]);
// const arc = arg.extract(["a"]);

const Type1 = z.object({ a: z.string() }).merge(z.object({ a: z.number() }));
type test1 = z.infer<typeof Type1>;
// const aaa = z.object({ a: z.string(), b: z.number() });
// const bbb = aaa.extend({ b: z.string() });

const Type2 = Type1.merge(z.object({ b: z.string() }));
type test2 = z.infer<typeof Type2>;
// const Type1 = z.object({ a: z.string() }).merge(z.object({ a: z.number() }));
// type test1 = z.infer<typeof Type1>;

const Type3 = Type2.merge(z.object({ c: z.string() }));
type test3 = z.infer<typeof Type3>;
// const Type2 = Type1.merge(z.object({ b: z.string() }));
// type test2 = z.infer<typeof Type2>;

const Type4 = Type3.merge(z.object({ Type3: z.string() }));
type test4 = z.infer<typeof Type4>;
// const Type3 = Type2.merge(z.object({ c: z.string() }));
// type test3 = z.infer<typeof Type3>;

const Type5 = Type4.merge(z.object({ Type4: z.string() }));
type test5 = z.infer<typeof Type5>;
// const Type4 = Type3.merge(z.object({ Type3: z.string() }));
// type test4 = z.infer<typeof Type4>;

const Type6 = Type5.merge(z.object({ Type5: z.string() }));
type test6 = z.infer<typeof Type6>;
// const Type5 = Type4.merge(z.object({ Type4: z.string() }));
// type test5 = z.infer<typeof Type5>;

const Type7 = Type6.merge(z.object({ Type6: z.string() }));
type test7 = z.infer<typeof Type7>;
// const Type6 = Type5.merge(z.object({ Type5: z.string() }));
// type test6 = z.infer<typeof Type6>;

const Type8 = Type7.merge(z.object({ Type7: z.string() }));
type test8 = z.infer<typeof Type8>;
// const Type7 = Type6.merge(z.object({ Type6: z.string() }));
// type test7 = z.infer<typeof Type7>;

const Type9 = Type8.merge(z.object({ Type8: z.string() }));
type test9 = z.infer<typeof Type9>;
// const Type8 = Type7.merge(z.object({ Type7: z.string() }));
// type test8 = z.infer<typeof Type8>;

const Type10 = Type9.merge(z.object({ Type9: z.string() }));
type test10 = z.infer<typeof Type10>;
// const Type9 = Type8.merge(z.object({ Type8: z.string() }));
// type test9 = z.infer<typeof Type9>;

const Type11 = Type10.merge(z.object({ Type10: z.string() }));
type test11 = z.infer<typeof Type11>;
// const Type10 = Type9.merge(z.object({ Type9: z.string() }));
// type test10 = z.infer<typeof Type10>;

const Type12 = Type11.merge(z.object({ Type11: z.string() }));
type test12 = z.infer<typeof Type12>;
// const Type11 = Type10.merge(z.object({ Type10: z.string() }));
// type test11 = z.infer<typeof Type11>;

const Type13 = Type12.merge(z.object({ Type12: z.string() }));
type test13 = z.infer<typeof Type13>;
// const Type12 = Type11.merge(z.object({ Type11: z.string() }));
// type test12 = z.infer<typeof Type12>;

const Type14 = Type13.merge(z.object({ Type13: z.string() }));
type test14 = z.infer<typeof Type14>;
// const Type13 = Type12.merge(z.object({ Type12: z.string() }));
// type test13 = z.infer<typeof Type13>;

const Type15 = Type14.merge(z.object({ Type14: z.string() }));
type test15 = z.infer<typeof Type15>;
// const Type14 = Type13.merge(z.object({ Type13: z.string() }));
// type test14 = z.infer<typeof Type14>;

const Type16 = Type14.merge(z.object({ Type15: z.string() }));
type test16 = z.infer<typeof Type16>;
// const Type15 = Type14.merge(z.object({ Type14: z.string() }));
// type test15 = z.infer<typeof Type15>;

const arg = Type16.parse("asdf");
arg;
// const Type16 = Type14.merge(z.object({ Type15: z.string() }));
// type test16 = z.infer<typeof Type16>;

// const arg = Type16.parse("asdf");
// arg;
12 changes: 8 additions & 4 deletions src/types.ts
Expand Up @@ -3648,6 +3648,8 @@ export type FilterEnum<Values, ToExclude> = Values extends []
: [Head, ...FilterEnum<Rest, ToExclude>]
: never;

export type typecast<A, T> = A extends T ? A : never;

function createZodEnum<U extends string, T extends Readonly<[U, ...U[]]>>(

This comment has been minimized.

Copy link
@SimplyLinn

SimplyLinn Feb 8, 2023

The root cause of the issue this commit is resolving is the ordering of these overloads.

writeable arrays can always be used in readonly places, but not vice versa.

When typescript is figuring out which overload to use, it checks top-to-bottom (order dependent)

If you send in a writeable version of [U, ...U[]], it will still match with the top version, and use that signature, because you're saying "I will treat this as a readonly array" in the constraint, and since it doesn't matter if you don't try to write to a writeable array, typescript picks that one, and doesn't check deeper.

The lower one is NEVER going to match, because all cases that will match the lower one will already have matched this top one.

This caused excessive and complex nesting in the compiled .d.ts file, once everything got transpiled. This didn't pop up in the original .ts source files because at that time, the types were inferred by the method call .create-method, which I believe might be more permissive of nesting and other typing schenanigans, but when transpiled into an explicit return type, the issue popped up, as it became the faulty ZodEnum<Writeable<FilterEnum<T, ToExclude[number]>>> .

FilterEnum already, always, returns a writeable tuple, so it should really not go through the Writeable<> step. It would match the the writeable version of createZodEnum, which would result in a ZodEnum<FilterEnum<T, ToExclude[number]>> return type in the generated d.ts file, which typescript handles just fine without casting to any or other workarounds. But it doesn't even try, because it already matched the readonly version.

values: T,
params?: RawCreateParams
Expand Down Expand Up @@ -3724,19 +3726,21 @@ export class ZodEnum<T extends [string, ...string[]]> extends ZodType<

extract<ToExtract extends readonly [T[number], ...T[number][]]>(
values: ToExtract
) {
return ZodEnum.create(values);
): ZodEnum<Writeable<ToExtract>> {
return ZodEnum.create(values) as any;
}

exclude<ToExclude extends readonly [T[number], ...T[number][]]>(
values: ToExclude
) {
): ZodEnum<
typecast<Writeable<FilterEnum<T, ToExclude[number]>>, [string, ...string[]]>
> {
return ZodEnum.create(
this.options.filter((opt) => !values.includes(opt)) as FilterEnum<
T,
ToExclude[number]
>
);
) as any;
}

static create = createZodEnum;
Expand Down

0 comments on commit e71c7be

Please sign in to comment.