Skip to content
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

Fix path matching types getting lost in recursive types #354

Merged
merged 5 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wicked-peaches-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"inngest": patch
---

Fix path matching types getting lost in certain recursive event types
1 change: 1 addition & 0 deletions packages/inngest/etc/inngest.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

```ts

import { IsEqual } from 'type-fest';
import { Jsonify } from 'type-fest';
import { Simplify } from 'type-fest';
import { z as z_2 } from 'zod';
Expand Down
2 changes: 1 addition & 1 deletion packages/inngest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
"json-stringify-safe": "^5.0.1",
"ms": "^2.1.3",
"serialize-error-cjs": "^0.1.3",
"type-fest": "^3.5.1",
"type-fest": "^3.13.1",
"zod": "~3.21.4"
},
"devDependencies": {
Expand Down
42 changes: 42 additions & 0 deletions packages/inngest/src/components/EventSchemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -816,5 +816,47 @@ describe("EventSchemas", () => {
}
);
});

test("does not infinitely recurse when matching events with recursive types", () => {
type JsonObject = { [Key in string]?: JsonValue };
type JsonArray = Array<JsonValue>;
type JsonValue =
| string
| number
| boolean
| JsonObject
| JsonArray
| null;

interface TestEvent extends EventPayload {
name: "test.event";
data: { id: string; other: JsonValue; yer: string[] };
}

interface TestEvent2 extends EventPayload {
name: "test.event2";
data: { id: string; somethingElse: JsonValue };
}

const schemas = new EventSchemas().fromUnion<TestEvent | TestEvent2>();

const inngest = new Inngest({
id: "test",
schemas,
eventKey: "test-key-123",
});

inngest.createFunction(
{ id: "test", cancelOn: [{ event: "test.event2", match: "data.id" }] },
{ event: "test.event" },
({ step }) => {
void step.waitForEvent("id", {
event: "test.event2",
match: "data.id",
timeout: "1h",
});
}
);
});
});
});
67 changes: 55 additions & 12 deletions packages/inngest/src/helpers/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Simplify } from "type-fest";
import { type IsEqual, type Simplify } from "type-fest";
import { type EventPayload } from "../types";

/**
Expand Down Expand Up @@ -35,24 +35,67 @@ export type SendEventPayload<Events extends Record<string, EventPayload>> =
* A list of simple, JSON-compatible, primitive types that contain no other
* values.
*/
export type Primitive = string | number | boolean | undefined | null;
export type Primitive =
| null
| undefined
| string
| number
| boolean
| symbol
| bigint;

/**
* Given a key and a value, create a string that would be used to access that
* property in code.
* Returns `true` if `T` is a tuple, else `false`.
*/
type StringPath<K extends string | number, V> = V extends Primitive
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type IsTuple<T extends ReadonlyArray<any>> = number extends T["length"]
? false
: true;

/**
* Given a tuple `T`, return the keys of that tuple, excluding any shared or
* generic keys like `number` and standard array methods.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type TupleKeys<T extends ReadonlyArray<any>> = Exclude<keyof T, keyof any[]>;

/**
* Returns `true` if `T1` matches anything in the union `T2`, else` never`.
*/
type AnyIsEqual<T1, T2> = T1 extends T2
? IsEqual<T1, T2> extends true
? true
: never
: never;

/**
* A helper for concatenating an existing path `K` with new paths from the
* value `V`, making sure to skip those we've already seen in
* `TraversedTypes`.
*
* Purposefully skips some primitive objects to avoid building unsupported or
* recursive paths.
*/
type PathImpl<K extends string | number, V, TraversedTypes> = V extends
| Primitive
| Date
? `${K}`
: true extends AnyIsEqual<TraversedTypes, V>
? `${K}`
: `${K}` | `${K}.${Path<V>}`;
: `${K}` | `${K}.${PathInternal<V, TraversedTypes | V>}`;

/**
* Given an object or array, recursively return all string paths used to access
* properties within those objects.
* Start iterating over a given object `T` and return all string paths used to
* access properties within that object as if you were in code.
*/
type Path<T> = T extends Array<infer V>
? StringPath<number, V>
type PathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<infer V>
? IsTuple<T> extends true
? {
[K in TupleKeys<T>]-?: PathImpl<K & string, T[K], TraversedTypes>;
}[TupleKeys<T>]
: PathImpl<number, V, TraversedTypes>
: {
[K in keyof T]-?: StringPath<K & string, T[K]>;
[K in keyof T]-?: PathImpl<K & string, T[K], TraversedTypes>;
}[keyof T];

/**
Expand All @@ -63,7 +106,7 @@ type Path<T> = T extends Array<infer V>
* paths of known objects.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ObjectPaths<T extends Record<string, any>> = Path<T>;
export type ObjectPaths<T> = T extends any ? PathInternal<T> : never;

/**
* Returns all keys from objects in the union `T`.
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.