Skip to content

Commit c825246

Browse files
authored
Replacing instanceof in traverse (#2580)
Instead of #2574 might be concluding one
1 parent e15e2fe commit c825246

3 files changed

Lines changed: 74 additions & 64 deletions

File tree

express-zod-api/src/common-helpers.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { $ZodType } from "@zod/core";
1+
import type { $ZodObject, $ZodTransform, $ZodType } from "@zod/core";
22
import { Request } from "express";
33
import * as R from "ramda";
44
import { globalRegistry, z } from "zod";
@@ -85,9 +85,15 @@ export const getMessageFromError = (error: Error): string => {
8585
return error.message;
8686
};
8787

88+
/** Faster replacement to instanceof for code operating core types (traversing schemas) */
89+
export const isSchema = <T extends $ZodType>(
90+
subject: $ZodType,
91+
type: T["_zod"]["def"]["type"],
92+
): subject is T => subject._zod.def.type === type;
93+
8894
/** Takes the original unvalidated examples from the properties of ZodObject schema shape */
89-
export const pullExampleProps = <T extends z.ZodObject>(subject: T) =>
90-
Object.entries(subject.shape).reduce<Partial<z.input<T>>[]>(
95+
export const pullExampleProps = <T extends $ZodObject>(subject: T) =>
96+
Object.entries(subject._zod.def.shape).reduce<Partial<z.input<T>>[]>(
9197
(acc, [key, schema]) => {
9298
const { examples = [] } = globalRegistry.get(schema)?.[metaSymbol] || {};
9399
return combinations(acc, examples.map(R.objOf(key)), ([left, right]) => ({
@@ -127,7 +133,7 @@ export const getExamples = <
127133
pullProps?: boolean;
128134
}): ReadonlyArray<V extends "parsed" ? z.output<T> : z.input<T>> => {
129135
let examples = globalRegistry.get(schema)?.[metaSymbol]?.examples || [];
130-
if (!examples.length && pullProps && schema instanceof z.ZodObject)
136+
if (!examples.length && pullProps && isSchema<$ZodObject>(schema, "object"))
131137
examples = pullExampleProps(schema);
132138
if (!validate && variant === "original") return examples;
133139
const result: Array<z.input<T> | z.output<T>> = [];
@@ -159,11 +165,20 @@ export const makeCleanId = (...args: string[]) => {
159165
};
160166

161167
export const getTransformedType = R.tryCatch(
162-
<T>(schema: z.ZodTransform<unknown, T>, sample: T) =>
163-
typeof schema.parse(sample),
168+
<T>(schema: $ZodTransform<unknown, T>, sample: T) =>
169+
typeof z.parse(schema, sample),
164170
R.always(undefined),
165171
);
166172

173+
/** @link https://github.com/colinhacks/zod/issues/4159 */
174+
export const doesAccept = R.tryCatch(
175+
(schema: $ZodType, value: undefined | null) => {
176+
z.parse(schema, value);
177+
return true;
178+
},
179+
R.always(false),
180+
);
181+
167182
/** @desc can still be an array, use Array.isArray() or rather R.type() to exclude that case */
168183
export const isObject = (subject: unknown) =>
169184
typeof subject === "object" && subject !== null;

express-zod-api/src/documentation-helpers.ts

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import type { $ZodPipe, $ZodTuple, $ZodType, JSONSchema } from "@zod/core";
1+
import type {
2+
$ZodPipe,
3+
$ZodTransform,
4+
$ZodTuple,
5+
$ZodType,
6+
JSONSchema,
7+
} from "@zod/core";
28
import {
39
ExamplesObject,
410
isReferenceObject,
@@ -20,10 +26,12 @@ import { globalRegistry, z } from "zod";
2026
import { ResponseVariant } from "./api-response";
2127
import {
2228
combinations,
29+
doesAccept,
2330
FlatObject,
2431
getExamples,
2532
getRoutePathParams,
2633
getTransformedType,
34+
isSchema,
2735
makeCleanId,
2836
routePathParamsRegex,
2937
Tag,
@@ -263,28 +271,24 @@ export const onPipeline: Overrider = ({ zodSchema, jsonSchema }, ctx) => {
263271
const opposite = (zodSchema as $ZodPipe)._zod.def[
264272
ctx.isResponse ? "in" : "out"
265273
];
266-
if (target instanceof z.ZodTransform) {
267-
const opposingDepiction = depict(opposite, { ctx });
268-
if (isSchemaObject(opposingDepiction)) {
269-
if (!ctx.isResponse) {
270-
const { type: opposingType, ...rest } = opposingDepiction;
274+
if (!isSchema<$ZodTransform>(target, "transform")) return;
275+
const opposingDepiction = depict(opposite, { ctx });
276+
if (isSchemaObject(opposingDepiction)) {
277+
if (!ctx.isResponse) {
278+
const { type: opposingType, ...rest } = opposingDepiction;
279+
Object.assign(jsonSchema, {
280+
...rest,
281+
format: `${rest.format || opposingType} (preprocessed)`,
282+
});
283+
} else {
284+
const targetType = getTransformedType(
285+
target,
286+
makeSample(opposingDepiction),
287+
);
288+
if (targetType && ["number", "string", "boolean"].includes(targetType)) {
271289
Object.assign(jsonSchema, {
272-
...rest,
273-
format: `${rest.format || opposingType} (preprocessed)`,
290+
type: targetType as "number" | "string" | "boolean",
274291
});
275-
} else {
276-
const targetType = getTransformedType(
277-
target,
278-
makeSample(opposingDepiction),
279-
);
280-
if (
281-
targetType &&
282-
["number", "string", "boolean"].includes(targetType)
283-
) {
284-
Object.assign(jsonSchema, {
285-
type: targetType as "number" | "string" | "boolean",
286-
});
287-
}
288292
}
289293
}
290294
}
@@ -404,9 +408,7 @@ export const depictRequestParams = ({
404408
name,
405409
in: location,
406410
deprecated: globalRegistry.get(paramSchema)?.deprecated,
407-
required: !(
408-
paramSchema instanceof z.ZodType && paramSchema.isOptional()
409-
),
411+
required: !doesAccept(paramSchema, undefined),
410412
description: depicted.description || description,
411413
schema: result,
412414
examples: depictParamExamples(objectSchema, name),
@@ -433,16 +435,11 @@ const overrides: Partial<Record<FirstPartyKind | ProprietaryBrand, Overrider>> =
433435
};
434436

435437
const onEach: Overrider = ({ zodSchema, jsonSchema }, { isResponse }) => {
436-
const shouldAvoidParsing =
437-
zodSchema._zod.def.type === "lazy" || zodSchema._zod.def.type === "promise";
438-
const hasTypePropertyInDepiction = jsonSchema.type !== undefined;
439-
const acceptsNull =
438+
if (
440439
!isResponse &&
441-
!shouldAvoidParsing &&
442-
hasTypePropertyInDepiction &&
443-
zodSchema instanceof z.ZodType &&
444-
zodSchema.isNullable();
445-
if (acceptsNull)
440+
jsonSchema.type !== undefined &&
441+
doesAccept(zodSchema, null)
442+
)
446443
Object.assign(jsonSchema, { type: makeNullableType(jsonSchema) });
447444
const examples = getExamples({
448445
schema: zodSchema,

express-zod-api/src/zts.ts

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ import type {
1414
$ZodPipe,
1515
$ZodReadonly,
1616
$ZodRecord,
17+
$ZodString,
18+
$ZodTransform,
1719
$ZodTuple,
1820
$ZodUnion,
1921
} from "@zod/core";
2022
import * as R from "ramda";
2123
import ts from "typescript";
2224
import { globalRegistry, z } from "zod";
23-
import { getTransformedType } from "./common-helpers";
25+
import { doesAccept, getTransformedType, isSchema } from "./common-helpers";
2426
import { ezDateInBrand } from "./date-in-schema";
2527
import { ezDateOutBrand } from "./date-out-schema";
2628
import { ezFileBrand, FileSchema } from "./file-schema";
@@ -92,10 +94,8 @@ const onObject: Producer = (
9294
const members = Object.entries(def.shape).map<ts.TypeElement>(
9395
([key, value]) => {
9496
const isOptional = isResponse
95-
? value._zod.def.type === "optional"
96-
: value._zod.def.type !== "promise" &&
97-
value instanceof z.ZodType &&
98-
value.isOptional();
97+
? isSchema<$ZodOptional>(value, "optional")
98+
: doesAccept(value, undefined);
9999
const { description: comment, deprecated: isDeprecated } =
100100
globalRegistry.get(value) || {};
101101
return makeInterfaceProp(key, next(value), {
@@ -184,24 +184,22 @@ const onPipeline: Producer = (
184184
) => {
185185
const target = def[isResponse ? "out" : "in"];
186186
const opposite = def[isResponse ? "in" : "out"];
187-
if (target instanceof z.ZodTransform) {
188-
const opposingType = next(opposite);
189-
const targetType = getTransformedType(target, makeSample(opposingType));
190-
const resolutions: Partial<
191-
Record<NonNullable<typeof targetType>, ts.KeywordTypeSyntaxKind>
192-
> = {
193-
number: ts.SyntaxKind.NumberKeyword,
194-
bigint: ts.SyntaxKind.BigIntKeyword,
195-
boolean: ts.SyntaxKind.BooleanKeyword,
196-
string: ts.SyntaxKind.StringKeyword,
197-
undefined: ts.SyntaxKind.UndefinedKeyword,
198-
object: ts.SyntaxKind.ObjectKeyword,
199-
};
200-
return ensureTypeNode(
201-
(targetType && resolutions[targetType]) || ts.SyntaxKind.AnyKeyword,
202-
);
203-
}
204-
return next(target);
187+
if (!isSchema<$ZodTransform>(target, "transform")) return next(target);
188+
const opposingType = next(opposite);
189+
const targetType = getTransformedType(target, makeSample(opposingType));
190+
const resolutions: Partial<
191+
Record<NonNullable<typeof targetType>, ts.KeywordTypeSyntaxKind>
192+
> = {
193+
number: ts.SyntaxKind.NumberKeyword,
194+
bigint: ts.SyntaxKind.BigIntKeyword,
195+
boolean: ts.SyntaxKind.BooleanKeyword,
196+
string: ts.SyntaxKind.StringKeyword,
197+
undefined: ts.SyntaxKind.UndefinedKeyword,
198+
object: ts.SyntaxKind.ObjectKeyword,
199+
};
200+
return ensureTypeNode(
201+
(targetType && resolutions[targetType]) || ts.SyntaxKind.AnyKeyword,
202+
);
205203
};
206204

207205
const onNull: Producer = () => makeLiteralType(null);
@@ -213,9 +211,9 @@ const onFile: Producer = (schema: FileSchema) => {
213211
const stringType = ensureTypeNode(ts.SyntaxKind.StringKeyword);
214212
const bufferType = ensureTypeNode("Buffer");
215213
const unionType = f.createUnionTypeNode([stringType, bufferType]);
216-
return schema._zod.def.type === "string"
214+
return isSchema<$ZodString>(schema, "string")
217215
? stringType
218-
: schema._zod.def.type === "union"
216+
: isSchema<$ZodUnion>(schema, "union")
219217
? unionType
220218
: bufferType;
221219
};

0 commit comments

Comments
 (0)