Skip to content

Commit

Permalink
Improve registration types (#25983)
Browse files Browse the repository at this point in the history
The error messages seem better with these new types where we no longer use overloads: instead of displaying every overload when none match we get a more specific error.

GitOrigin-RevId: 7a1049cd869f55655d7491e1f3550a9ce52ecb71
  • Loading branch information
thomasballinger authored and Convex, Inc. committed Jun 13, 2024
1 parent 7a6d453 commit 734bce8
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 97 deletions.
24 changes: 12 additions & 12 deletions src/server/impl/registration_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ function exportReturns(functionDefinition: FunctionDefinition) {
*
* @public
*/
export const mutationGeneric: MutationBuilder<any, "public"> = (
export const mutationGeneric: MutationBuilder<any, "public"> = ((
functionDefinition: FunctionDefinition,
) => {
const func = (
Expand All @@ -182,7 +182,7 @@ export const mutationGeneric: MutationBuilder<any, "public"> = (
func.exportArgs = exportArgs(functionDefinition);
func.exportReturns = exportReturns(functionDefinition);
return func;
};
}) as MutationBuilder<any, "public">;

/**
* Define a mutation that is only accessible from other Convex functions (but not from the client).
Expand All @@ -197,7 +197,7 @@ export const mutationGeneric: MutationBuilder<any, "public"> = (
*
* @public
*/
export const internalMutationGeneric: MutationBuilder<any, "internal"> = (
export const internalMutationGeneric: MutationBuilder<any, "internal"> = ((
functionDefinition: FunctionDefinition,
) => {
const func = (
Expand All @@ -218,7 +218,7 @@ export const internalMutationGeneric: MutationBuilder<any, "internal"> = (
func.exportArgs = exportArgs(functionDefinition);
func.exportReturns = exportReturns(functionDefinition);
return func;
};
}) as MutationBuilder<any, "internal">;

async function invokeQuery<
F extends (ctx: GenericQueryCtx<GenericDataModel>, ...args: any) => any,
Expand Down Expand Up @@ -250,7 +250,7 @@ async function invokeQuery<
*
* @public
*/
export const queryGeneric: QueryBuilder<any, "public"> = (
export const queryGeneric: QueryBuilder<any, "public"> = ((
functionDefinition: FunctionDefinition,
) => {
const func = (
Expand All @@ -271,7 +271,7 @@ export const queryGeneric: QueryBuilder<any, "public"> = (
func.exportArgs = exportArgs(functionDefinition);
func.exportReturns = exportReturns(functionDefinition);
return func;
};
}) as QueryBuilder<any, "public">;

/**
* Define a query that is only accessible from other Convex functions (but not from the client).
Expand All @@ -286,7 +286,7 @@ export const queryGeneric: QueryBuilder<any, "public"> = (
*
* @public
*/
export const internalQueryGeneric: QueryBuilder<any, "internal"> = (
export const internalQueryGeneric: QueryBuilder<any, "internal"> = ((
functionDefinition: FunctionDefinition,
) => {
const func = (
Expand All @@ -307,7 +307,7 @@ export const internalQueryGeneric: QueryBuilder<any, "internal"> = (
func.exportArgs = exportArgs(functionDefinition);
func.exportReturns = exportReturns(functionDefinition);
return func;
};
}) as QueryBuilder<any, "internal">;

async function invokeAction<
F extends (ctx: GenericActionCtx<GenericDataModel>, ...args: any) => any,
Expand Down Expand Up @@ -336,7 +336,7 @@ async function invokeAction<
*
* @public
*/
export const actionGeneric: ActionBuilder<any, "public"> = (
export const actionGeneric: ActionBuilder<any, "public"> = ((
functionDefinition: FunctionDefinition,
) => {
const func = (
Expand All @@ -358,7 +358,7 @@ export const actionGeneric: ActionBuilder<any, "public"> = (
func.exportArgs = exportArgs(functionDefinition);
func.exportReturns = exportReturns(functionDefinition);
return func;
};
}) as ActionBuilder<any, "public">;

/**
* Define an action that is only accessible from other Convex functions (but not from the client).
Expand All @@ -371,7 +371,7 @@ export const actionGeneric: ActionBuilder<any, "public"> = (
*
* @public
*/
export const internalActionGeneric: ActionBuilder<any, "internal"> = (
export const internalActionGeneric: ActionBuilder<any, "internal"> = ((
functionDefinition: FunctionDefinition,
) => {
const func = (
Expand All @@ -393,7 +393,7 @@ export const internalActionGeneric: ActionBuilder<any, "internal"> = (
func.exportArgs = exportArgs(functionDefinition);
func.exportReturns = exportReturns(functionDefinition);
return func;
};
}) as ActionBuilder<any, "internal">;

async function invokeHttpAction<
F extends (ctx: GenericActionCtx<GenericDataModel>, request: Request) => any,
Expand Down
21 changes: 16 additions & 5 deletions src/server/registration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,25 @@ describe("argument inference", () => {
return "result";
},
}),
// This error could be prettier if we stop overloading the builders.
// @ts-expect-error The arg type mismatches
configValidatorMismatchedTypedArg: mutation({
args: {
_arg: v.number(),
},
// @ts-expect-error The arg type mismatches
handler: (_, { _arg }: { _arg: string }) => {
return "result";
},
}),
configValidatorReturn: mutation({
args: {
_arg: v.number(),
},
returns: v.number(),
// @ts-expect-error The return type mismatches
handler: (_, { _arg }) => {
return "result";
},
}),

// These are unusual syntaxes. We'd like to break some of them.
// Let's document them here so it's clear when we do that.
Expand All @@ -100,6 +109,7 @@ describe("argument inference", () => {
},
}),
configTypedOptionalDefaultArg: mutation({
// This syntax is incidentally allowed, it is not supported.
handler: (_, { arg }: { arg?: string } = { arg: "default" }) => {
assert<Equals<typeof arg, string | undefined>>;
return "result";
Expand All @@ -109,7 +119,6 @@ describe("argument inference", () => {
args: {
arg: v.string(),
},
// @ts-expect-error This syntax has never been allowed.
handler: (_, { arg } = { arg: "default" }) => {
assert<Equals<typeof arg, string>>;
return "result";
Expand Down Expand Up @@ -139,6 +148,8 @@ describe("argument inference", () => {
test("inline with no arg", () => {
type Args = API["module"]["inlineNoArg"]["_args"];
assert<Equals<Args, EmptyObject>>();
type ReturnType = API["module"]["inlineNoArg"]["_returnType"];
assert<Equals<ReturnType, string>>();
});

test("inline with untyped arg", () => {
Expand All @@ -164,7 +175,7 @@ describe("argument inference", () => {
// This syntax is a type error where it is defined so it falls back.
test("inline with typed arg with default value", () => {
type Args = API["module"]["inlineTypedDefaultArg"]["_args"];
type ExpectedArgs = Record<string, any>;
type ExpectedArgs = Record<string, unknown>;
assert<Equals<Args, ExpectedArgs>>;
});

Expand Down Expand Up @@ -229,7 +240,7 @@ describe("argument inference", () => {
test("config with typed arg and a default", () => {
type Args = API["module"]["configTypedDefaultArg"]["_args"];
// This is a type error at the definition site so this is the fallback.
type ExpectedArgs = Record<string, any>;
type ExpectedArgs = Record<string, unknown>;
assert<Equals<Args, ExpectedArgs>>;
});

Expand Down
171 changes: 91 additions & 80 deletions src/server/registration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ValidatorTypeToReturnType,
} from "../server/api.js";
import {
Infer,
ObjectType,
PropertyValidators,
Validator,
Expand Down Expand Up @@ -479,6 +480,28 @@ export interface ArgsAndReturnsValidatedFunction<
) => ValidatorTypeToReturnType<ReturnsValidator["type"]>;
}

// These types use a tuple of length 1 to avoid distribution over the union
// https://github.com/microsoft/TypeScript/issues/29368#issuecomment-453529532
// We also use `void` instead of `undefined` so that this works with `strictNullChecks: false`
// which would treat `T | undefined` as equivalent to `T`.
type ReturnValueForOptionalValidator<
ReturnsValidator extends Validator<any, any, any> | void,
> = [ReturnsValidator] extends [Validator<any, any, any>]
? ValidatorTypeToReturnType<Infer<ReturnsValidator>>
: any;

type ArgsArrayForOptionalValidator<
ArgsValidator extends PropertyValidators | void,
> = [ArgsValidator] extends [PropertyValidators]
? OneArgArray<ObjectType<ArgsValidator>>
: ArgsArray;

type DefaultArgsForOptionalValidator<
ArgsValidator extends PropertyValidators | void,
> = [ArgsValidator] extends [PropertyValidators]
? [ObjectType<ArgsValidator>]
: OneArgArray;

/**
* Internal type helper used by Convex code generation.
*
Expand All @@ -489,38 +512,36 @@ export type MutationBuilder<
DataModel extends GenericDataModel,
Visibility extends FunctionVisibility,
> = {
<Returns, ArgsValidator extends PropertyValidators>(
func: ValidatedFunction<
GenericMutationCtx<DataModel>,
ArgsValidator,
Returns
>,
): RegisteredMutation<Visibility, ObjectType<ArgsValidator>, Returns>;

/**
* Validate the arguments and return value of a function.
* Useful for generating API specs for functions.
*
* @internal
*/
<
ReturnsValidator extends Validator<any, any, any>,
ArgsValidator extends PropertyValidators,
ArgsValidator extends PropertyValidators | void,
ReturnsValidator extends Validator<any, any, any> | void,
ReturnValue extends ReturnValueForOptionalValidator<ReturnsValidator> = any,
OneOrZeroArgs extends
ArgsArrayForOptionalValidator<ArgsValidator> = DefaultArgsForOptionalValidator<ArgsValidator>,
>(
func: ArgsAndReturnsValidatedFunction<
GenericMutationCtx<DataModel>,
ArgsValidator,
ReturnsValidator
>,
mutation:
| {
args?: ArgsValidator;
returns?: ReturnsValidator;
handler: (
ctx: GenericMutationCtx<DataModel>,
...args: OneOrZeroArgs
) => ReturnValue;
}
| {
// args and returns could be allowed here but this doesn't work at runtime today
//args?: ArgsValidator;
//returns?: ReturnsValidator;
(
ctx: GenericMutationCtx<DataModel>,
...args: OneOrZeroArgs
): ReturnValue;
},
): RegisteredMutation<
Visibility,
ObjectType<ArgsValidator>,
ValidatorTypeToReturnType<ReturnsValidator["type"]>
ArgsArrayToObject<OneOrZeroArgs>,
ReturnValue
>;

<Returns, Args extends ArgsArray = OneArgArray>(
func: UnvalidatedFunction<GenericMutationCtx<DataModel>, Args, Returns>,
): RegisteredMutation<Visibility, ArgsArrayToObject<Args>, Returns>;
};

/**
Expand All @@ -533,34 +554,29 @@ export type QueryBuilder<
DataModel extends GenericDataModel,
Visibility extends FunctionVisibility,
> = {
<Returns, ArgsValidator extends PropertyValidators>(
func: ValidatedFunction<GenericQueryCtx<DataModel>, ArgsValidator, Returns>,
): RegisteredQuery<Visibility, ObjectType<ArgsValidator>, Returns>;

/**
* Validate the arguments and return value of a function.
* Useful for generating API specs for functions.
*
* @internal
*/
<
ReturnsValidator extends Validator<any, any, any>,
ArgsValidator extends PropertyValidators,
ArgsValidator extends PropertyValidators | void,
ReturnsValidator extends Validator<any, any, any> | void,
ReturnValue extends ReturnValueForOptionalValidator<ReturnsValidator> = any,
OneOrZeroArgs extends
ArgsArrayForOptionalValidator<ArgsValidator> = DefaultArgsForOptionalValidator<ArgsValidator>,
>(
func: ArgsAndReturnsValidatedFunction<
GenericQueryCtx<DataModel>,
ArgsValidator,
ReturnsValidator
>,
): RegisteredQuery<
Visibility,
ObjectType<ArgsValidator>,
ValidatorTypeToReturnType<ReturnsValidator["type"]>
>;

<Returns, Args extends ArgsArray = OneArgArray>(
func: UnvalidatedFunction<GenericQueryCtx<DataModel>, Args, Returns>,
): RegisteredQuery<Visibility, ArgsArrayToObject<Args>, Returns>;
query:
| {
args?: ArgsValidator;
returns?: ReturnsValidator;
handler: (
ctx: GenericQueryCtx<DataModel>,
...args: OneOrZeroArgs
) => ReturnValue;
}
| {
(
ctx: GenericQueryCtx<DataModel>,
...args: OneOrZeroArgs
): ReturnValue;
},
): RegisteredQuery<Visibility, ArgsArrayToObject<OneOrZeroArgs>, ReturnValue>;
};

/**
Expand All @@ -573,38 +589,33 @@ export type ActionBuilder<
DataModel extends GenericDataModel,
Visibility extends FunctionVisibility,
> = {
<Returns, ArgsValidator extends PropertyValidators>(
func: ValidatedFunction<
GenericActionCtx<DataModel>,
ArgsValidator,
Returns
>,
): RegisteredAction<Visibility, ObjectType<ArgsValidator>, Returns>;

/**
* Validate the arguments and return value of a function.
* Useful for generating API specs for functions.
*
* @internal
*/
<
ReturnsValidator extends Validator<any, any, any>,
ArgsValidator extends PropertyValidators,
ArgsValidator extends PropertyValidators | void,
ReturnsValidator extends Validator<any, any, any> | void,
ReturnValue extends ReturnValueForOptionalValidator<ReturnsValidator> = any,
OneOrZeroArgs extends
ArgsArrayForOptionalValidator<ArgsValidator> = DefaultArgsForOptionalValidator<ArgsValidator>,
>(
func: ArgsAndReturnsValidatedFunction<
GenericActionCtx<DataModel>,
ArgsValidator,
ReturnsValidator
>,
func:
| {
args?: ArgsValidator;
returns?: ReturnsValidator;
handler: (
ctx: GenericActionCtx<DataModel>,
...args: OneOrZeroArgs
) => ReturnValue;
}
| {
(
ctx: GenericActionCtx<DataModel>,
...args: OneOrZeroArgs
): ReturnValue;
},
): RegisteredAction<
Visibility,
ObjectType<ArgsValidator>,
ValidatorTypeToReturnType<ReturnsValidator["type"]>
ArgsArrayToObject<OneOrZeroArgs>,
ReturnValue
>;

<Returns, Args extends ArgsArray = OneArgArray>(
func: UnvalidatedFunction<GenericActionCtx<DataModel>, Args, Returns>,
): RegisteredAction<Visibility, ArgsArrayToObject<Args>, Returns>;
};

/**
Expand Down

0 comments on commit 734bce8

Please sign in to comment.