Skip to content

Commit

Permalink
Improve IDE integration for inferred object properties to support Go …
Browse files Browse the repository at this point in the history
…To Definition, Rename Symbol, etc. (#1117)

* Increase test coverage for type inference of optional object properties

* Add test showing that Go To Definition is broken on object properties

* Fix Go To Definition for inferred object properties

* Fix Go To Definition for inferred merged object properties

* Improve languageServerFeatures test names

* Fix Go To Definition for inferred pick object properties

* Fix Go To Definition for inferred omit object properties

* Exclude languageServerFeatures tests from deno

* Fix keyof issue

* 3.14.5

* Remove tc dep

Co-authored-by: Colin McDonnell <colinmcd@alum.mit.edu>
  • Loading branch information
bentefay and Colin McDonnell committed May 6, 2022
1 parent 4cf7e7c commit fa4eebb
Show file tree
Hide file tree
Showing 9 changed files with 458 additions and 48 deletions.
6 changes: 5 additions & 1 deletion deno/build.mjs
Expand Up @@ -23,7 +23,11 @@ const projectRoot = process.cwd();
const nodeSrcRoot = join(projectRoot, "src");
const denoLibRoot = join(projectRoot, "deno", "lib");

const skipList = [join(nodeSrcRoot, "__tests__", "object-in-es5-env.test.ts")];
const skipList = [
join(nodeSrcRoot, "__tests__", "object-in-es5-env.test.ts"),
join(nodeSrcRoot, "__tests__", "languageServerFeatures.test.ts"),
join(nodeSrcRoot, "__tests__", "languageServerFeatures.source.ts"),
];
const walkAndBuild = (/** @type string */ dir) => {
for (const entry of readdirSync(join(nodeSrcRoot, dir), {
withFileTypes: true,
Expand Down
52 changes: 52 additions & 0 deletions deno/lib/__tests__/object.test.ts
Expand Up @@ -216,6 +216,46 @@ test("test inferred merged type", async () => {
f1;
});

test("inferred merged object type with optional properties", async () => {
const Merged = z
.object({ a: z.string(), b: z.string().optional() })
.merge(z.object({ a: z.string().optional(), b: z.string() }));
type Merged = z.infer<typeof Merged>;
const f1: util.AssertEqual<Merged, { a?: string; b: string }> = true;
f1;
});

test("inferred unioned object type with optional properties", async () => {
const Unioned = z.union([
z.object({ a: z.string(), b: z.string().optional() }),
z.object({ a: z.string().optional(), b: z.string() }),
]);
type Unioned = z.infer<typeof Unioned>;
const f1: util.AssertEqual<
Unioned,
{ a: string; b?: string } | { a?: string; b: string }
> = true;
f1;
});

test("inferred partial object type with optional properties", async () => {
const Partial = z
.object({ a: z.string(), b: z.string().optional() })
.partial();
type Partial = z.infer<typeof Partial>;
const f1: util.AssertEqual<Partial, { a?: string; b?: string }> = true;
f1;
});

test("inferred picked object type with optional properties", async () => {
const Picked = z
.object({ a: z.string(), b: z.string().optional() })
.pick({ b: true });
type Picked = z.infer<typeof Picked>;
const f1: util.AssertEqual<Picked, { b?: string }> = true;
f1;
});

test("inferred type for unknown/any keys", () => {
const myType = z.object({
anyOptional: z.any().optional(),
Expand Down Expand Up @@ -308,3 +348,15 @@ test("constructor key", () => {
})
).toThrow();
});

test("constructor key", () => {
const Example = z.object({
prop: z.string(),
opt: z.number().optional(),
arr: z.string().array(),
});

type Example = z.infer<typeof Example>;
const f1: util.AssertEqual<keyof Example, "prop" | "opt" | "arr"> = true;
f1;
});
25 changes: 5 additions & 20 deletions deno/lib/types.ts
Expand Up @@ -1363,17 +1363,12 @@ export namespace objectUtil {
[k in Exclude<keyof U, keyof V>]: U[k];
} & V;

type optionalKeys<T extends object> = {
[k in keyof T]: undefined extends T[k] ? k : never;
}[keyof T];

// type requiredKeys<T extends object> = Exclude<keyof T, optionalKeys<T>>;
type requiredKeys<T extends object> = {
export type requiredKeys<T extends object> = {
[k in keyof T]: undefined extends T[k] ? never : k;
}[keyof T];

export type addQuestionMarks<T extends object> = {
[k in optionalKeys<T>]?: T[k];
[k in keyof T]?: T[k];
} & { [k in requiredKeys<T>]: T[k] };

export type identity<T> = T;
Expand All @@ -1398,9 +1393,7 @@ export namespace objectUtil {
};
}

export type extendShape<A, B> = {
[k in Exclude<keyof A, keyof B>]: A[k];
} & { [k in keyof B]: B[k] };
export type extendShape<A, B> = Omit<A, keyof B> & B;

const AugmentFactory =
<Def extends ZodObjectDef>(def: Def) =>
Expand Down Expand Up @@ -1712,11 +1705,7 @@ export class ZodObject<

pick<Mask extends { [k in keyof T]?: true }>(
mask: Mask
): ZodObject<
objectUtil.noNever<{ [k in keyof Mask]: k extends keyof T ? T[k] : never }>,
UnknownKeys,
Catchall
> {
): ZodObject<Pick<T, Extract<keyof T, keyof Mask>>, UnknownKeys, Catchall> {
const shape: any = {};
util.objectKeys(mask).map((key) => {
shape[key] = this.shape[key];
Expand All @@ -1729,11 +1718,7 @@ export class ZodObject<

omit<Mask extends { [k in keyof T]?: true }>(
mask: Mask
): ZodObject<
objectUtil.noNever<{ [k in keyof T]: k extends keyof Mask ? never : T[k] }>,
UnknownKeys,
Catchall
> {
): ZodObject<Omit<T, keyof Mask>, UnknownKeys, Catchall> {
const shape: any = {};
util.objectKeys(this.shape).map((key) => {
if (util.objectKeys(mask).indexOf(key) === -1) {
Expand Down
3 changes: 2 additions & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "zod",
"version": "3.14.4",
"version": "3.14.5",
"description": "TypeScript-first schema declaration and validation library with static type inference",
"main": "./lib/index.js",
"types": "./index.d.ts",
Expand Down Expand Up @@ -87,6 +87,7 @@
"pretty-quick": "^3.1.3",
"rollup": "^2.70.1",
"ts-jest": "^27.1.3",
"ts-morph": "^14.0.0",
"ts-node": "^10.7.0",
"tslib": "^2.3.1",
"typescript": "^4.6.2"
Expand Down
76 changes: 76 additions & 0 deletions src/__tests__/languageServerFeatures.source.ts
@@ -0,0 +1,76 @@
import * as z from "../index";

export const filePath = __filename;

// z.object()

export const Test = z.object({
f1: z.number(),
});

export type Test = z.infer<typeof Test>;

export const instanceOfTest: Test = {
f1: 1,
};

// z.object().merge()

export const TestMerge = z
.object({
f2: z.string().optional(),
})
.merge(Test);

export type TestMerge = z.infer<typeof TestMerge>;

export const instanceOfTestMerge: TestMerge = {
f1: 1,
f2: "string",
};

// z.union()

export const TestUnion = z.union([
z.object({
f2: z.string().optional(),
}),
Test,
]);

export type TestUnion = z.infer<typeof TestUnion>;

export const instanceOfTestUnion: TestUnion = {
f1: 1,
f2: "string",
};

// z.object().partial()

export const TestPartial = Test.partial();

export type TestPartial = z.infer<typeof TestPartial>;

export const instanceOfTestPartial: TestPartial = {
f1: 1,
};

// z.object().pick()

export const TestPick = TestMerge.pick({ f1: true });

export type TestPick = z.infer<typeof TestPick>;

export const instanceOfTestPick: TestPick = {
f1: 1,
};

// z.object().omit()

export const TestOmit = TestMerge.omit({ f2: true });

export type TestOmit = z.infer<typeof TestOmit>;

export const instanceOfTestOmit: TestOmit = {
f1: 1,
};

0 comments on commit fa4eebb

Please sign in to comment.