Skip to content
Draft
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
6 changes: 3 additions & 3 deletions lib/types/request.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export interface RequestInfo {
* * fingerprint - the route internal normalized string representing the normalized path.
* [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-requestroute)
*/
export interface RequestRoute<Refs extends ReqRef = ReqRefDefaults> {
export interface RequestRoute<Refs extends ReqRef = {}> {
/** the route HTTP method. */
method: Exclude<Lowercase<HTTP_METHODS>, 'head'> | '*';

Expand Down Expand Up @@ -314,7 +314,7 @@ export type MergeRefs<T extends ReqRef> = MergeType<ReqRefDefaults, T>;
* HTTP server callback (which is available via [request.raw.req](https://github.com/hapijs/hapi/blob/master/API.md#request.raw)). The request properties change throughout
* the request [lifecycle](https://github.com/hapijs/hapi/blob/master/API.md#request-lifecycle).
*/
export interface Request<Refs extends ReqRef = ReqRefDefaults> extends Podium {
export interface Request<out Refs extends ReqRef = {}> extends Podium {
/**
* Application-specific state. Provides a safe place to store application data without potential conflicts with the framework. Should not be used by plugins which should use plugins[name].
* [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-requestapp)
Expand Down Expand Up @@ -404,7 +404,7 @@ export interface Request<Refs extends ReqRef = ReqRefDefaults> extends Podium {
/**
* An array containing all the path params values in the order they appeared in the path.
*/
readonly paramsArray: keyof MergeRefs<Refs>['Params'] | string[];
readonly paramsArray: string[];

/**
* The request URI's pathname component.
Expand Down
2 changes: 1 addition & 1 deletion lib/types/response.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ export interface Auth<
* document the h notation is used. It is named in the spirit of the RethinkDB r method, with h for hapi.
* [See docs](https://github.com/hapijs/hapi/blob/master/API.md#response-toolkit)
*/
export interface ResponseToolkit<Refs extends ReqRef = ReqRefDefaults> {
export interface ResponseToolkit<out Refs extends ReqRef = {}> {
/**
* A response symbol. When returned by a lifecycle method, the request lifecycle skips to the finalizing step
* without further interaction with the node response stream. It is the developer's responsibility to write
Expand Down
20 changes: 10 additions & 10 deletions lib/types/route.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,12 +336,12 @@ export interface RouteOptionsPayload {
/**
* For context [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-routeoptionspre)
*/
export type RouteOptionsPreArray<Refs extends ReqRef = ReqRefDefaults> = RouteOptionsPreAllOptions<Refs>[];
export type RouteOptionsPreArray<Refs extends ReqRef = {}> = RouteOptionsPreAllOptions<Refs>[];

/**
* For context [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-routeoptionspre)
*/
export type RouteOptionsPreAllOptions<Refs extends ReqRef = ReqRefDefaults> = RouteOptionsPreObject<Refs> | RouteOptionsPreObject<Refs>[] | Lifecycle.Method<Refs>;
export type RouteOptionsPreAllOptions<Refs extends ReqRef = {}> = RouteOptionsPreObject<Refs> | RouteOptionsPreObject<Refs>[] | Lifecycle.Method<Refs>;

/**
* An object with:
Expand All @@ -350,15 +350,15 @@ export type RouteOptionsPreAllOptions<Refs extends ReqRef = ReqRefDefaults> = Ro
* * failAction - A failAction value which determine what to do when a pre-handler method throws an error. If assign is specified and the failAction setting is not 'error', the error will be assigned.
* For context [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-routeoptionspre)
*/
export interface RouteOptionsPreObject<Refs extends ReqRef = ReqRefDefaults> {
export interface RouteOptionsPreObject<Refs extends ReqRef = {}> {
/**
* a lifecycle method.
*/
method: Lifecycle.Method<Refs>;
/**
* key name used to assign the response of the method to in request.pre and request.preResponses.
*/
assign?: keyof MergeRefs<Refs>['Pres'] | undefined;
assign?: Extract<keyof MergeRefs<Refs>['Pres'], string> | (string & {}) | undefined;
/**
* A failAction value which determine what to do when a pre-handler method throws an error. If assign is specified and the failAction setting is not 'error', the error will be assigned.
*/
Expand Down Expand Up @@ -640,7 +640,7 @@ export interface RouteOptionsValidate {
state?: RouteOptionsResponseSchema | undefined;
}

export interface CommonRouteProperties<Refs extends ReqRef = ReqRefDefaults> {
export interface CommonRouteProperties<Refs extends ReqRef = {}> {
/**
* Application-specific route configuration state. Should not be used by plugins which should use options.plugins[name] instead.
* [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-routeoptionsapp)
Expand Down Expand Up @@ -886,15 +886,15 @@ export interface AuthSettings {
access?: AccessSetting[] | undefined;
}

export interface RouteSettings<Refs extends ReqRef = ReqRefDefaults> extends CommonRouteProperties<Refs> {
export interface RouteSettings<Refs extends ReqRef = {}> extends CommonRouteProperties<Refs> {
auth?: AuthSettings | undefined;
}

/**
* Each route can be customized to change the default behavior of the request lifecycle.
* For context [See docs](https://github.com/hapijs/hapi/blob/master/API.md#route-options)
*/
export interface RouteOptions<Refs extends ReqRef = ReqRefDefaults> extends CommonRouteProperties<Refs> {
export interface RouteOptions<Refs extends ReqRef = {}> extends CommonRouteProperties<Refs> {
/**
* Route authentication configuration. Value can be:
* false to disable authentication if a default strategy is set.
Expand All @@ -915,14 +915,14 @@ export interface RulesInfo {
vhost: string;
}

export interface RulesOptions<Refs extends ReqRef = ReqRefDefaults> {
export interface RulesOptions<Refs extends ReqRef = {}> {
validate: {
schema?: ObjectSchema<MergeRefs<Refs>['Rules']> | Record<keyof MergeRefs<Refs>['Rules'], Schema> | undefined;
options?: ValidationOptions | undefined;
};
}

export interface RulesProcessor<Refs extends ReqRef = ReqRefDefaults> {
export interface RulesProcessor<Refs extends ReqRef = {}> {
(rules: MergeRefs<Refs>['Rules'] | null, info: RulesInfo): Partial<RouteOptions<Refs>> | null;
}

Expand All @@ -943,7 +943,7 @@ type RouteDefMethods = Exclude<HTTP_METHODS | Lowercase<HTTP_METHODS>, 'HEAD' |
* * rules - route custom rules object. The object is passed to each rules processor registered with server.rules(). Cannot be used if route.options.rules is defined.
* For context [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverrouteroute)
*/
export interface ServerRoute<Refs extends ReqRef = ReqRefDefaults> {
export interface ServerRoute<Refs extends ReqRef = {}> {
/**
* (required) the absolute path used to match incoming requests (must begin with '/'). Incoming requests are compared to the configured paths based on the server's router configuration. The path
* can include named parameters enclosed in {} which will be matched against literal values in the request as described in Path parameters. For context [See
Expand Down
8 changes: 4 additions & 4 deletions lib/types/server/auth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export type ServerAuthScheme<
// tslint:disable-next-line no-unnecessary-generics
Options extends ServerAuthSchemeOptions = ServerAuthSchemeOptions,
// tslint:disable-next-line no-unnecessary-generics
Refs extends ReqRef = ReqRefDefaults
Refs extends ReqRef = {}
> = (server: Server, options?: Options) => ServerAuthSchemeObject<Refs>;

export interface ServerAuthSchemeObjectApi {}
Expand All @@ -36,7 +36,7 @@ export interface ServerAuthSchemeObjectApi {}
* [See docs](https://github.com/hapijs/hapi/blob/master/API.md#authentication-scheme)
*/

export interface ServerAuthSchemeObject<Refs extends ReqRef = ReqRefDefaults> {
export interface ServerAuthSchemeObject<Refs extends ReqRef = {}> {
/**
* optional object which is exposed via the [server.auth.api](https://github.com/hapijs/hapi/blob/master/API.md#server.auth.api) object.
*/
Expand Down Expand Up @@ -155,7 +155,7 @@ export interface ServerAuth {
*/

scheme <
Refs extends ReqRef = ReqRefDefaults,
Refs extends ReqRef = {},
Options extends object = {}
// tslint:disable-next-line no-unnecessary-generics
>(name: string, scheme: ServerAuthScheme<Options, Refs>): void;
Expand Down Expand Up @@ -197,5 +197,5 @@ export interface ServerAuth {
* entity, or other route properties.
*/
// tslint:disable-next-line no-unnecessary-generics
verify <Refs extends ReqRef = ReqRefDefaults>(request: Request<Refs>): Promise<void>;
verify <Refs extends ReqRef = {}>(request: Request<Refs>): Promise<void>;
}
2 changes: 1 addition & 1 deletion lib/types/server/ext.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export interface ServerExtEventsObject<A = ServerApplicationState> {
options?: ServerExtOptions | undefined;
}

export interface RouteExtObject<Refs extends ReqRef = ReqRefDefaults> {
export interface RouteExtObject<Refs extends ReqRef = {}> {
method: Lifecycle.Method<Refs>;
options?: ServerExtOptions | undefined;
}
Expand Down
4 changes: 2 additions & 2 deletions lib/types/server/server.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ export class Server<A = ServerApplicationState> {
* Note that the options object is deeply cloned (with the exception of bind which is shallowly copied) and cannot contain any values that are unsafe to perform deep copy on.
* [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverrouteroute)
*/
route <Refs extends ReqRef = ReqRefDefaults>(route: ServerRoute<Refs> | ServerRoute<Refs>[]): void;
route <Refs extends ReqRef = {}>(route: ServerRoute<Refs> | ServerRoute<Refs>[]): void;

/**
* Defines a route rules processor for converting route rules object into route configuration where:
Expand All @@ -642,7 +642,7 @@ export class Server<A = ServerApplicationState> {
* @return void
* [See docs](https://github.com/hapijs/hapi/blob/master/API.md#-serverrulesprocessor-options)
*/
rules <Refs extends ReqRef = ReqRefDefaults>(
rules <Refs extends ReqRef = {}>(
processor: RulesProcessor<Refs>,
options?: RulesOptions<Refs> | undefined
): void;
Expand Down
16 changes: 11 additions & 5 deletions lib/types/utils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,20 @@ export namespace Lifecycle {
* * err - an error object available only when the method is used as a failAction value.
*/
type Method<
Refs extends ReqRef = ReqRefDefaults,
Refs extends ReqRef = {},
R extends ReturnValue<any> = ReturnValue<Refs>
> = (
> = {
// Declared as a method and immediately indexed so signature comparisons are
// bivariant (method signatures are exempt from strictFunctionTypes). A method
// typed with narrower Refs is an assertion about what validation guarantees,
// so it must remain assignable to surfaces typed with default or wider Refs.
bivariance(
this: MergeRefs<Refs>['Bind'],
request: Request<Refs>,
h: ResponseToolkit<Refs>,
err?: Error | undefined
) => R;
): R;
}['bivariance'];

/**
* Each lifecycle method must return a value or a promise that resolves into a value. If a lifecycle method returns
Expand All @@ -81,8 +87,8 @@ export namespace Lifecycle {
* - a promise object that resolve to any of the above values
* For more info please [See docs](https://github.com/hapijs/hapi/blob/master/API.md#lifecycle-methods)
*/
type ReturnValue<Refs extends ReqRef = ReqRefDefaults> = ReturnValueTypes<Refs> | (Promise<ReturnValueTypes<Refs>>);
type ReturnValueTypes<Refs extends ReqRef = ReqRefDefaults> =
type ReturnValue<Refs extends ReqRef = {}> = ReturnValueTypes<Refs> | (Promise<ReturnValueTypes<Refs>>);
type ReturnValueTypes<Refs extends ReqRef = {}> =
(null | string | number | boolean) |
(Buffer) |
(Error | Boom) |
Expand Down
128 changes: 121 additions & 7 deletions test/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,15 +478,12 @@ interface MyRouteRefs {
Query: { expand: string };
}

// KNOWN LIMITATION: Request<MyRouteRefs> is not assignable to Request<ReqRefDefaults>
// because TypeScript checks generic interface compatibility invariantly when
// the generic appears in contravariant positions (e.g. lifecycle method parameters).
// Workaround: use a generic function like processAuthGeneric<Refs> above instead
// of concrete Request (no generic) for helper functions that need to accept
// requests with different Refs.
// FIXED: Request is covariant in Refs (`out` annotation, default of `{}`),
// so a request with narrower refs is assignable to helpers typed with plain
// Request. The generic-function workaround (processAuthGeneric above) is no
// longer required.
export function issueConcreteVsGeneric(req: Request<MyRouteRefs>): void {

// @ts-expect-error - Known TS limitation: Request<CustomRefs> not assignable to Request<ReqRefDefaults>
concreteHelper(req);
}

Expand Down Expand Up @@ -515,3 +512,120 @@ const issueStateAny: ServerRoute = {
return 'ok';
}
};

// -----------------------------------------------------------------------------
// ISSUE 7: Refs mismatches between lifecycle methods and their surfaces
//
// FIXED: Lifecycle.Method is bivariant (method-signature exemption from
// strictFunctionTypes) and Request/ResponseToolkit are covariant in Refs with
// a default of `{}`. Consequences, each exercised below:
// - a method typed with narrower Refs attaches to surfaces typed with the
// defaults: server.ext(), route-level ext, failAction, pre;
// - a route typed with a superset of refs accepts methods typed with any
// subset of those refs (reusable pres);
// - an untyped route infers its Refs from a typed handler;
// - contradictory refs and default-to-narrow flows still fail.
// -----------------------------------------------------------------------------

interface VariancePayload {
email: string;
}

interface VarianceUser {
id: string;
}

const typedPayloadMethod: Lifecycle.Method<{ Payload: VariancePayload }> = (request) => {

check.type<VariancePayload>(request.payload);
return 'ok';
};

const typedPreMethod: Lifecycle.Method<{ Pres: { user: VarianceUser } }> = (request) => {

check.type<VarianceUser>(request.pre.user);
return 'ok';
};

// A method with narrower Refs attaches to default-typed extension points

server.ext('onPreHandler', typedPayloadMethod);

const varianceDefaultSurfaces: ServerRoute = {
method: 'POST',
path: '/variance/default-surfaces',
options: {
validate: {
payload: true,
failAction: typedPayloadMethod
},
ext: {
onPreHandler: { method: typedPayloadMethod }
},
handler: () => 'ok'
}
};

// A route typed with the superset of refs accepts subset-typed methods

const varianceSuperset: ServerRoute<{
Payload: VariancePayload;
Pres: { user: VarianceUser };
}> = {
method: 'POST',
path: '/variance/superset',
options: {
pre: [
{ method: typedPayloadMethod, assign: 'user' },
{ method: typedPreMethod }
],
handler: typedPayloadMethod
}
};

// An untyped route infers its Refs from the typed methods it is given

server.route({
method: 'POST',
path: '/variance/inferred',
options: {
pre: [{ method: typedPayloadMethod, assign: 'login' }],
handler: typedPayloadMethod
}
});

// paramsArray holds the param VALUES in path order — always strings

const varianceParamsArray: ServerRoute = {
method: 'GET',
path: '/variance/{id}',
handler: (request, h) => {

check.type<string[]>(request.paramsArray);
return 'ok';
}
};

// Contradictory refs on the same slot must still fail

const conflictingMethod: Lifecycle.Method<{ Payload: { count: number[] } }> = () => 'ok';

const varianceConflict: ServerRoute<{ Payload: VariancePayload }> = {
method: 'POST',
path: '/variance/conflict',
options: {
pre: [
// @ts-expect-error - contradictory Payload refs are not assignable
{ method: conflictingMethod, assign: 'user' }
],
handler: typedPayloadMethod
}
};

// A default request must still not satisfy narrower refs

export function varianceDefaultToNarrow(req: Request): void {

// @ts-expect-error - Request (defaults) does not satisfy narrower refs
check.type<Request<{ Payload: VariancePayload }>>(req);
}
Loading