RFC: Exportable types
See original GitHub issueOne request we’ve had is to provide user importable types from this package—i.e. a way to use worker types without declaring it in tsconfig.json
. This is easy enough for most of our types (ExecutionContext
, for instance):
// types.d.ts
export interface ExecutionContext {
waitUntil(promise: Promise<any>): void;
passThroughOnException(): void;
}
// index.ts
import { ExecutionContext } from "./types.d.ts"
The rest of this issues will address options for providing importable types for globals like Response
, Request
, and fetch
, which are both values and types.
Is this a problem to solve?
The workers runtime does have the globals Response
etc…, and so in some sense using ambient declarations is the “right” approach (as this package does currently). However, there are downsides, including the lack of automatic inclusion of @types/*
packages, and the fact that workers can’t be typed correctly in frameworks like Remix, which mix browser code and worker code in the same project (and even file!).
Here’s a very simple example of a Remix file that doesn’t typecheck, because Response.json()
is non standard. It runs fine when published, as Response.json()
is available in the workers runtime.
import { useLoaderData } from '@remix-run/react';
export const loader = async () => {
return Response.json({
data: [
{
slug: 'my-first-post',
title: 'My First Post'
}
]
});
};
export default function Index() {
const { data } = useLoaderData();
return <div>{JSON.stringify(data)}</div>;
}
How about just…exporting the types?
At first glance, just adding an export
keyword would seem to solve the issue:
// types.d.ts
export declare class Response extends Body {
...
}
// index.ts
import { Response } from "./types.d.ts"
Indeed, this typechecks fine, but doesn’t build fine (esbuild
can’t find the Response
export).
An honestly pretty reasonable option for classes
// types.d.ts
declare class Response extends Body {
...
}
class WorkerResponse extends Response {}
export {WorkerResponse as Response}
// index.ts
import { Response } from "./types.d.ts"
This both typechecks and builds with esbuild
, as well as (as far as I can tell) working completely fine in the runtime.
It also provides a solution for the Remix example given earlier:
```ts
import { useLoaderData } from '@remix-run/react';
import { Response as WorkerResponse } from './types.d.ts';
export const loader = async () => {
return WorkerResponse.json({
data: [
{
slug: 'my-first-post',
title: 'My First Post'
}
]
});
};
export default function Index() {
const { data } = useLoaderData();
return <div>{JSON.stringify(data)}</div>;
}
Frankenstein’s monster
One type remains; the function:
declare function fetch(
request: Request | string,
requestInitr?: RequestInit | Request
): Promise<Response>;
I propose the following solution:
// types.d.ts
declare function fetch(
request: Request | string,
requestInitr?: RequestInit | Request
): Promise<Response>;
// @ts-ignore next-line
const WorkerFetch: typeof fetch = globalThis.fetch
export {
WorkerFetch as fetch
}
// @ts-ignore next-line
is needed becauseglobalThis
isn’t typed. Can we typeglobalThis
, you may ask? No, since that would injectfetch
as a global type, which is exactly what this approach is trying to avoid.
This typechecks, builds, and runs correctly, but is a bit…horrifying. If anyone has any suggestions on how to improve this I’m all ears!
Issue Analytics
- State:
- Created a year ago
- Reactions:2
- Comments:6 (6 by maintainers)
@GregBrimble
const Response = globalThis.Response
is just a value. To get the type ofResponse
, you’d need to usetypeof Response
, which breaks the usual expectation for classes. That’s an okay tradeoff for functions, because you needtypeof
for functions anyway (I’m pretty sure, at least).We should be able to close all these issues if we do this: