Skip to content

Commit

Permalink
Add generic type for opaque object (#3385)
Browse files Browse the repository at this point in the history
This adds a `TOpaque` generic type parameter to the type definitions
for request(), connect(), stream(), and pipeline(). The type parameter
defaults to null, which is the default value of the opaque property.
If an opaque value is passed in the options, its type can usually be
inferred automatically, such that no explicit type declaration is
necessary. This commit also adds tsd tests to make sure the type
definitions work as expected.

Previously, the type of `opaque` was `unknown`, which means it needed
to be either type-checked or casted to another type before anything
could be done with it. Such code should not be broken by this commit,
although some type checks or assertions might become redundant. Code
that disabled type checks (e.g. by casting to `any` or using
`@ts-ignore` should be unaffected. Code that does not use typescript
at all is also unaffected.

This closes #3378
  • Loading branch information
jfhr committed Jul 9, 2024
1 parent 98dae4e commit 10caf6d
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 40 deletions.
12 changes: 11 additions & 1 deletion test/types/api.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Duplex, Readable, Writable } from 'stream'
import { expectAssignable } from 'tsd'
import { expectAssignable, expectType } from 'tsd'
import { Dispatcher, request, stream, pipeline, connect, upgrade } from '../..'

// request
Expand All @@ -10,12 +10,22 @@ expectAssignable<Promise<Dispatcher.ResponseData>>(request('', { method: 'GET',
// stream
expectAssignable<Promise<Dispatcher.StreamData>>(stream('', { method: 'GET' }, data => {
expectAssignable<Dispatcher.StreamFactoryData>(data)
expectType<null>(data.opaque)
return new Writable()
}))
expectAssignable<Promise<Dispatcher.StreamData<{ example: string }>>>(stream('', { method: 'GET', opaque: { example: '' } }, data => {
expectType<{ example: string }>(data.opaque)
return new Writable()
}))

// pipeline
expectAssignable<Duplex>(pipeline('', { method: 'GET' }, data => {
expectAssignable<Dispatcher.PipelineHandlerData>(data)
expectType<null>(data.opaque)
return new Readable()
}))
expectAssignable<Duplex>(pipeline('', { method: 'GET', opaque: { example: '' } }, data => {
expectType<{ example: string }>(data.opaque)
return new Readable()
}))

Expand Down
22 changes: 22 additions & 0 deletions test/types/dispatcher.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ expectAssignable<Dispatcher>(new Dispatcher())
}))
expectAssignable<Promise<Dispatcher.ResponseData>>(dispatcher.request({ origin: '', path: '', method: 'GET', responseHeaders: 'raw' }))
expectAssignable<Promise<Dispatcher.ResponseData>>(dispatcher.request({ origin: '', path: '', method: 'GET', responseHeaders: null }))
expectAssignable<Promise<Dispatcher.ResponseData<{ example: string }>>>(dispatcher.request({ origin: '', path: '', method: 'GET', opaque: { example: '' } }))

// pipeline
expectAssignable<Duplex>(dispatcher.pipeline({ origin: '', path: '', method: 'GET', maxRedirections: 0 }, data => {
Expand All @@ -84,6 +85,11 @@ expectAssignable<Dispatcher>(new Dispatcher())
expectAssignable<Dispatcher.PipelineHandlerData>(data)
return new Readable()
}))
expectAssignable<Duplex>(dispatcher.pipeline({ origin: '', path: '', method: 'GET', opaque: { example: '' } }, data => {
expectAssignable<Dispatcher.PipelineHandlerData<{ example: string }>>(data)
expectType<{ example: string }>(data.opaque)
return new Readable()
}))

// stream
expectAssignable<Promise<Dispatcher.StreamData>>(dispatcher.stream({ origin: '', path: '', method: 'GET', maxRedirections: 0 }, data => {
Expand All @@ -94,6 +100,10 @@ expectAssignable<Dispatcher>(new Dispatcher())
expectAssignable<Dispatcher.StreamFactoryData>(data)
return new Writable()
}))
expectAssignable<Promise<Dispatcher.StreamData<{ example: string }>>>(dispatcher.stream({ origin: '', path: '', method: 'GET', opaque: { example: '' } }, data => {
expectType<{ example: string }>(data.opaque);
return new Writable();
}));
expectAssignable<void>(dispatcher.stream(
{ origin: '', path: '', method: 'GET', reset: false },
data => {
Expand All @@ -116,6 +126,18 @@ expectAssignable<Dispatcher>(new Dispatcher())
expectAssignable<Dispatcher.StreamData>(data)
}
))
expectAssignable<void>(dispatcher.stream(
{ origin: new URL('http://localhost'), path: '', method: 'GET', opaque: { example: '' } },
data => {
expectAssignable<Dispatcher.StreamFactoryData<{ example: string }>>(data)
return new Writable()
},
(err, data) => {
expectAssignable<Error | null>(err)
expectAssignable<Dispatcher.StreamData<{ example: string }>>(data)
expectType<{ example: string }>(data.opaque)
}
))
expectAssignable<Promise<Dispatcher.StreamData>>(dispatcher.stream({ origin: '', path: '', method: 'GET', responseHeaders: 'raw' }, data => {
expectAssignable<Dispatcher.StreamFactoryData>(data)
return new Writable()
Expand Down
26 changes: 13 additions & 13 deletions types/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,30 @@ export {
}

/** Performs an HTTP request. */
declare function request(
declare function request<TOpaque = null>(
url: string | URL | UrlObject,
options?: { dispatcher?: Dispatcher } & Omit<Dispatcher.RequestOptions, 'origin' | 'path' | 'method'> & Partial<Pick<Dispatcher.RequestOptions, 'method'>>,
): Promise<Dispatcher.ResponseData>;
options?: { dispatcher?: Dispatcher } & Omit<Dispatcher.RequestOptions<TOpaque>, 'origin' | 'path' | 'method'> & Partial<Pick<Dispatcher.RequestOptions, 'method'>>,
): Promise<Dispatcher.ResponseData<TOpaque>>;

/** A faster version of `request`. */
declare function stream(
declare function stream<TOpaque = null>(
url: string | URL | UrlObject,
options: { dispatcher?: Dispatcher } & Omit<Dispatcher.RequestOptions, 'origin' | 'path'>,
factory: Dispatcher.StreamFactory
): Promise<Dispatcher.StreamData>;
options: { dispatcher?: Dispatcher } & Omit<Dispatcher.RequestOptions<TOpaque>, 'origin' | 'path'>,
factory: Dispatcher.StreamFactory<TOpaque>
): Promise<Dispatcher.StreamData<TOpaque>>;

/** For easy use with `stream.pipeline`. */
declare function pipeline(
declare function pipeline<TOpaque = null>(
url: string | URL | UrlObject,
options: { dispatcher?: Dispatcher } & Omit<Dispatcher.PipelineOptions, 'origin' | 'path'>,
handler: Dispatcher.PipelineHandler
options: { dispatcher?: Dispatcher } & Omit<Dispatcher.PipelineOptions<TOpaque>, 'origin' | 'path'>,
handler: Dispatcher.PipelineHandler<TOpaque>
): Duplex;

/** Starts two-way communications with the requested resource. */
declare function connect(
declare function connect<TOpaque = null>(
url: string | URL | UrlObject,
options?: { dispatcher?: Dispatcher } & Omit<Dispatcher.ConnectOptions, 'origin' | 'path'>
): Promise<Dispatcher.ConnectData>;
options?: { dispatcher?: Dispatcher } & Omit<Dispatcher.ConnectOptions<TOpaque>, 'origin' | 'path'>
): Promise<Dispatcher.ConnectData<TOpaque>>;

/** Upgrade to a different protocol. */
declare function upgrade(
Expand Down
52 changes: 26 additions & 26 deletions types/dispatcher.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@ declare class Dispatcher extends EventEmitter {
/** Dispatches a request. This API is expected to evolve through semver-major versions and is less stable than the preceding higher level APIs. It is primarily intended for library developers who implement higher level APIs on top of this. */
dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean;
/** Starts two-way communications with the requested resource. */
connect(options: Dispatcher.ConnectOptions): Promise<Dispatcher.ConnectData>;
connect(options: Dispatcher.ConnectOptions, callback: (err: Error | null, data: Dispatcher.ConnectData) => void): void;
connect<TOpaque = null>(options: Dispatcher.ConnectOptions<TOpaque>): Promise<Dispatcher.ConnectData<TOpaque>>;
connect<TOpaque = null>(options: Dispatcher.ConnectOptions<TOpaque>, callback: (err: Error | null, data: Dispatcher.ConnectData<TOpaque>) => void): void;
/** Compose a chain of dispatchers */
compose(dispatchers: Dispatcher.DispatcherComposeInterceptor[]): Dispatcher.ComposedDispatcher;
compose(...dispatchers: Dispatcher.DispatcherComposeInterceptor[]): Dispatcher.ComposedDispatcher;
/** Performs an HTTP request. */
request(options: Dispatcher.RequestOptions): Promise<Dispatcher.ResponseData>;
request(options: Dispatcher.RequestOptions, callback: (err: Error | null, data: Dispatcher.ResponseData) => void): void;
request<TOpaque = null>(options: Dispatcher.RequestOptions<TOpaque>): Promise<Dispatcher.ResponseData<TOpaque>>;
request<TOpaque = null>(options: Dispatcher.RequestOptions<TOpaque>, callback: (err: Error | null, data: Dispatcher.ResponseData<TOpaque>) => void): void;
/** For easy use with `stream.pipeline`. */
pipeline(options: Dispatcher.PipelineOptions, handler: Dispatcher.PipelineHandler): Duplex;
pipeline<TOpaque = null>(options: Dispatcher.PipelineOptions<TOpaque>, handler: Dispatcher.PipelineHandler<TOpaque>): Duplex;
/** A faster version of `Dispatcher.request`. */
stream(options: Dispatcher.RequestOptions, factory: Dispatcher.StreamFactory): Promise<Dispatcher.StreamData>;
stream(options: Dispatcher.RequestOptions, factory: Dispatcher.StreamFactory, callback: (err: Error | null, data: Dispatcher.StreamData) => void): void;
stream<TOpaque = null>(options: Dispatcher.RequestOptions<TOpaque>, factory: Dispatcher.StreamFactory<TOpaque>): Promise<Dispatcher.StreamData<TOpaque>>;
stream<TOpaque = null>(options: Dispatcher.RequestOptions<TOpaque>, factory: Dispatcher.StreamFactory<TOpaque>, callback: (err: Error | null, data: Dispatcher.StreamData<TOpaque>) => void): void;
/** Upgrade to a different protocol. */
upgrade(options: Dispatcher.UpgradeOptions): Promise<Dispatcher.UpgradeData>;
upgrade(options: Dispatcher.UpgradeOptions, callback: (err: Error | null, data: Dispatcher.UpgradeData) => void): void;
Expand Down Expand Up @@ -125,25 +125,25 @@ declare namespace Dispatcher {
/** For H2, it appends the expect: 100-continue header, and halts the request body until a 100-continue is received from the remote server*/
expectContinue?: boolean;
}
export interface ConnectOptions {
export interface ConnectOptions<TOpaque = null> {
origin: string | URL;
path: string;
/** Default: `null` */
headers?: IncomingHttpHeaders | string[] | null;
/** Default: `null` */
signal?: AbortSignal | EventEmitter | null;
/** This argument parameter is passed through to `ConnectData` */
opaque?: unknown;
opaque?: TOpaque;
/** Default: 0 */
maxRedirections?: number;
/** Default: false */
redirectionLimitReached?: boolean;
/** Default: `null` */
responseHeaders?: 'raw' | null;
}
export interface RequestOptions extends DispatchOptions {
export interface RequestOptions<TOpaque = null> extends DispatchOptions {
/** Default: `null` */
opaque?: unknown;
opaque?: TOpaque;
/** Default: `null` */
signal?: AbortSignal | EventEmitter | null;
/** Default: 0 */
Expand All @@ -157,7 +157,7 @@ declare namespace Dispatcher {
/** Default: `64 KiB` */
highWaterMark?: number;
}
export interface PipelineOptions extends RequestOptions {
export interface PipelineOptions<TOpaque = null> extends RequestOptions<TOpaque> {
/** `true` if the `handler` will return an object stream. Default: `false` */
objectMode?: boolean;
}
Expand All @@ -178,43 +178,43 @@ declare namespace Dispatcher {
/** Default: `null` */
responseHeaders?: 'raw' | null;
}
export interface ConnectData {
export interface ConnectData<TOpaque = null> {
statusCode: number;
headers: IncomingHttpHeaders;
socket: Duplex;
opaque: unknown;
opaque: TOpaque;
}
export interface ResponseData {
export interface ResponseData<TOpaque = null> {
statusCode: number;
headers: IncomingHttpHeaders;
body: BodyReadable & BodyMixin;
trailers: Record<string, string>;
opaque: unknown;
opaque: TOpaque;
context: object;
}
export interface PipelineHandlerData {
export interface PipelineHandlerData<TOpaque = null> {
statusCode: number;
headers: IncomingHttpHeaders;
opaque: unknown;
opaque: TOpaque;
body: BodyReadable;
context: object;
}
export interface StreamData {
opaque: unknown;
export interface StreamData<TOpaque = null> {
opaque: TOpaque;
trailers: Record<string, string>;
}
export interface UpgradeData {
export interface UpgradeData<TOpaque = null> {
headers: IncomingHttpHeaders;
socket: Duplex;
opaque: unknown;
opaque: TOpaque;
}
export interface StreamFactoryData {
export interface StreamFactoryData<TOpaque = null> {
statusCode: number;
headers: IncomingHttpHeaders;
opaque: unknown;
opaque: TOpaque;
context: object;
}
export type StreamFactory = (data: StreamFactoryData) => Writable;
export type StreamFactory<TOpaque = null> = (data: StreamFactoryData<TOpaque>) => Writable;
export interface DispatchHandlers {
/** Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails. */
onConnect?(abort: (err?: Error) => void): void;
Expand All @@ -233,7 +233,7 @@ declare namespace Dispatcher {
/** Invoked when a body chunk is sent to the server. May be invoked multiple times for chunked requests */
onBodySent?(chunkSize: number, totalBytesSent: number): void;
}
export type PipelineHandler = (data: PipelineHandlerData) => Readable;
export type PipelineHandler<TOpaque = null> = (data: PipelineHandlerData<TOpaque>) => Readable;
export type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH';

/**
Expand Down

0 comments on commit 10caf6d

Please sign in to comment.