diff --git a/attw.json b/attw.json
index 2cbbaab7d85e9d..639018244df1ef 100644
--- a/attw.json
+++ b/attw.json
@@ -164,6 +164,7 @@
"katex",
"keep-network__tbtc.js",
"koa",
+ "koa/v2",
"kss",
"leveldown",
"locutus",
diff --git a/types/koa/.npmignore b/types/koa/.npmignore
index 93e307400a5456..d73cba2e3a91b8 100644
--- a/types/koa/.npmignore
+++ b/types/koa/.npmignore
@@ -3,3 +3,4 @@
!**/*.d.cts
!**/*.d.mts
!**/*.d.*.ts
+/v2/
diff --git a/types/koa/v2/.eslintrc.json b/types/koa/v2/.eslintrc.json
new file mode 100644
index 00000000000000..c16f75a689eed8
--- /dev/null
+++ b/types/koa/v2/.eslintrc.json
@@ -0,0 +1,10 @@
+{
+ "rules": {
+ "@definitelytyped/no-unnecessary-generics": "off",
+ "@definitelytyped/strict-export-declare-modifiers": "off",
+ "@typescript-eslint/no-unsafe-function-type": "off",
+ "@typescript-eslint/no-wrapper-object-types": "off",
+ "@typescript-eslint/no-empty-interface": "off",
+ "@typescript-eslint/consistent-type-definitions": "off"
+ }
+}
diff --git a/types/koa/v2/.npmignore b/types/koa/v2/.npmignore
new file mode 100644
index 00000000000000..93e307400a5456
--- /dev/null
+++ b/types/koa/v2/.npmignore
@@ -0,0 +1,5 @@
+*
+!**/*.d.ts
+!**/*.d.cts
+!**/*.d.mts
+!**/*.d.*.ts
diff --git a/types/koa/v2/index.d.ts b/types/koa/v2/index.d.ts
new file mode 100644
index 00000000000000..d3cb0d11917634
--- /dev/null
+++ b/types/koa/v2/index.d.ts
@@ -0,0 +1,748 @@
+/* =================== USAGE ===================
+
+ import * as Koa from "koa"
+ const app = new Koa()
+
+ async function (ctx: Koa.Context, next: Koa.Next) {
+ // ...
+ }
+
+ =============================================== */
+///
+import accepts = require("accepts");
+import { AsyncLocalStorage } from "async_hooks";
+import Cookies = require("cookies");
+import { EventEmitter } from "events";
+import { IncomingHttpHeaders, IncomingMessage, OutgoingHttpHeaders, Server, ServerResponse } from "http";
+import { Http2ServerRequest, Http2ServerResponse } from "http2";
+import httpAssert = require("http-assert");
+import contentDisposition = require("content-disposition");
+import HttpErrors = require("http-errors");
+import Keygrip = require("keygrip");
+import compose = require("koa-compose");
+import { ListenOptions, Socket } from "net";
+import { ParsedUrlQuery } from "querystring";
+import * as url from "url";
+
+declare interface ContextDelegatedRequest {
+ /**
+ * Return request header.
+ */
+ header: IncomingHttpHeaders;
+
+ /**
+ * Return request header, alias as request.header
+ */
+ headers: IncomingHttpHeaders;
+
+ /**
+ * Get/Set request URL.
+ */
+ url: string;
+
+ /**
+ * Get origin of URL.
+ */
+ origin: string;
+
+ /**
+ * Get full request URL.
+ */
+ href: string;
+
+ /**
+ * Get/Set request method.
+ */
+ method: string;
+
+ /**
+ * Get request pathname.
+ * Set pathname, retaining the query-string when present.
+ */
+ path: string;
+
+ /**
+ * Get parsed query-string.
+ * Set query-string as an object.
+ */
+ query: ParsedUrlQuery;
+
+ /**
+ * Get/Set query string.
+ */
+ querystring: string;
+
+ /**
+ * Get the search string. Same as the querystring
+ * except it includes the leading ?.
+ *
+ * Set the search string. Same as
+ * response.querystring= but included for ubiquity.
+ */
+ search: string;
+
+ /**
+ * Parse the "Host" header field host
+ * and support X-Forwarded-Host when a
+ * proxy is enabled.
+ */
+ host: string;
+
+ /**
+ * Parse the "Host" header field hostname
+ * and support X-Forwarded-Host when a
+ * proxy is enabled.
+ */
+ hostname: string;
+
+ /**
+ * Get WHATWG parsed URL object.
+ */
+ URL: url.URL;
+
+ /**
+ * Check if the request is fresh, aka
+ * Last-Modified and/or the ETag
+ * still match.
+ */
+ fresh: boolean;
+
+ /**
+ * Check if the request is stale, aka
+ * "Last-Modified" and / or the "ETag" for the
+ * resource has changed.
+ */
+ stale: boolean;
+
+ /**
+ * Check if the request is idempotent.
+ */
+ idempotent: boolean;
+
+ /**
+ * Return the request socket.
+ */
+ socket: Socket;
+
+ /**
+ * Return the protocol string "http" or "https"
+ * when requested with TLS. When the proxy setting
+ * is enabled the "X-Forwarded-Proto" header
+ * field will be trusted. If you're running behind
+ * a reverse proxy that supplies https for you this
+ * may be enabled.
+ */
+ protocol: string;
+
+ /**
+ * Short-hand for:
+ *
+ * this.protocol == 'https'
+ */
+ secure: boolean;
+
+ /**
+ * Request remote address. Supports X-Forwarded-For when app.proxy is true.
+ */
+ ip: string;
+
+ /**
+ * When `app.proxy` is `true`, parse
+ * the "X-Forwarded-For" ip address list.
+ *
+ * For example if the value were "client, proxy1, proxy2"
+ * you would receive the array `["client", "proxy1", "proxy2"]`
+ * where "proxy2" is the furthest down-stream.
+ */
+ ips: string[];
+
+ /**
+ * Return subdomains as an array.
+ *
+ * Subdomains are the dot-separated parts of the host before the main domain
+ * of the app. By default, the domain of the app is assumed to be the last two
+ * parts of the host. This can be changed by setting `app.subdomainOffset`.
+ *
+ * For example, if the domain is "tobi.ferrets.example.com":
+ * If `app.subdomainOffset` is not set, this.subdomains is
+ * `["ferrets", "tobi"]`.
+ * If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`.
+ */
+ subdomains: string[];
+
+ /**
+ * Check if the given `type(s)` is acceptable, returning
+ * the best match when true, otherwise `false`, in which
+ * case you should respond with 406 "Not Acceptable".
+ *
+ * The `type` value may be a single mime type string
+ * such as "application/json", the extension name
+ * such as "json" or an array `["json", "html", "text/plain"]`. When a list
+ * or array is given the _best_ match, if any is returned.
+ *
+ * Examples:
+ *
+ * // Accept: text/html
+ * this.accepts('html');
+ * // => "html"
+ *
+ * // Accept: text/*, application/json
+ * this.accepts('html');
+ * // => "html"
+ * this.accepts('text/html');
+ * // => "text/html"
+ * this.accepts('json', 'text');
+ * // => "json"
+ * this.accepts('application/json');
+ * // => "application/json"
+ *
+ * // Accept: text/*, application/json
+ * this.accepts('image/png');
+ * this.accepts('png');
+ * // => undefined
+ *
+ * // Accept: text/*;q=.5, application/json
+ * this.accepts(['html', 'json']);
+ * this.accepts('html', 'json');
+ * // => "json"
+ */
+ accepts(): string[];
+ accepts(...types: string[]): string | false;
+ accepts(types: string[]): string | false;
+
+ /**
+ * Return accepted encodings or best fit based on `encodings`.
+ *
+ * Given `Accept-Encoding: gzip, deflate`
+ * an array sorted by quality is returned:
+ *
+ * ['gzip', 'deflate']
+ */
+ acceptsEncodings(): string[];
+ acceptsEncodings(...encodings: string[]): string | false;
+ acceptsEncodings(encodings: string[]): string | false;
+
+ /**
+ * Return accepted charsets or best fit based on `charsets`.
+ *
+ * Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5`
+ * an array sorted by quality is returned:
+ *
+ * ['utf-8', 'utf-7', 'iso-8859-1']
+ */
+ acceptsCharsets(): string[];
+ acceptsCharsets(...charsets: string[]): string | false;
+ acceptsCharsets(charsets: string[]): string | false;
+
+ /**
+ * Return accepted languages or best fit based on `langs`.
+ *
+ * Given `Accept-Language: en;q=0.8, es, pt`
+ * an array sorted by quality is returned:
+ *
+ * ['es', 'pt', 'en']
+ */
+ acceptsLanguages(): string[];
+ acceptsLanguages(...langs: string[]): string | false;
+ acceptsLanguages(langs: string[]): string | false;
+
+ /**
+ * Check if the incoming request contains the "Content-Type"
+ * header field, and it contains any of the give mime `type`s.
+ * If there is no request body, `null` is returned.
+ * If there is no content type, `false` is returned.
+ * Otherwise, it returns the first `type` that matches.
+ *
+ * Examples:
+ *
+ * // With Content-Type: text/html; charset=utf-8
+ * this.is('html'); // => 'html'
+ * this.is('text/html'); // => 'text/html'
+ * this.is('text/*', 'application/json'); // => 'text/html'
+ *
+ * // When Content-Type is application/json
+ * this.is('json', 'urlencoded'); // => 'json'
+ * this.is('application/json'); // => 'application/json'
+ * this.is('html', 'application/*'); // => 'application/json'
+ *
+ * this.is('html'); // => false
+ */
+ // is(): string | boolean;
+ is(...types: string[]): string | false | null;
+ is(types: string[]): string | false | null;
+
+ /**
+ * Return request header. If the header is not set, will return an empty
+ * string.
+ *
+ * The `Referrer` header field is special-cased, both `Referrer` and
+ * `Referer` are interchangeable.
+ *
+ * Examples:
+ *
+ * this.get('Content-Type');
+ * // => "text/plain"
+ *
+ * this.get('content-type');
+ * // => "text/plain"
+ *
+ * this.get('Something');
+ * // => ''
+ */
+ get(field: string): string;
+}
+
+declare interface ContextDelegatedResponse {
+ /**
+ * Get/Set response status code.
+ */
+ status: number;
+
+ /**
+ * Get response status message
+ */
+ message: string;
+
+ /**
+ * Get/Set response body.
+ */
+ body: unknown;
+
+ /**
+ * Return parsed response Content-Length when present.
+ * Set Content-Length field to `n`.
+ */
+ length: number;
+
+ /**
+ * Check if a header has been written to the socket.
+ */
+ headerSent: boolean;
+
+ /**
+ * Vary on `field`.
+ */
+ vary(field: string | string[]): void;
+
+ /**
+ * Perform a 302 redirect to `url`.
+ *
+ * The string "back" is special-cased
+ * to provide Referrer support, when Referrer
+ * is not present `alt` or "/" is used.
+ *
+ * Examples:
+ *
+ * this.redirect('back');
+ * this.redirect('back', '/index.html');
+ * this.redirect('/login');
+ * this.redirect('http://google.com');
+ */
+ redirect(url: string, alt?: string): void;
+
+ /**
+ * Set Content-Disposition to "attachment" to signal the client to prompt for download.
+ * Optionally specify the filename of the download and some options.
+ */
+ attachment(filename?: string, options?: contentDisposition.Options): void;
+
+ /**
+ * Return the response mime type void of
+ * parameters such as "charset".
+ *
+ * Set Content-Type response header with `type` through `mime.lookup()`
+ * when it does not contain a charset.
+ *
+ * Examples:
+ *
+ * this.type = '.html';
+ * this.type = 'html';
+ * this.type = 'json';
+ * this.type = 'application/json';
+ * this.type = 'png';
+ */
+ type: string;
+
+ /**
+ * Get the Last-Modified date in Date form, if it exists.
+ * Set the Last-Modified date using a string or a Date.
+ *
+ * this.response.lastModified = new Date();
+ * this.response.lastModified = '2013-09-13';
+ */
+ lastModified: Date;
+
+ /**
+ * Get/Set the ETag of a response.
+ * This will normalize the quotes if necessary.
+ *
+ * this.response.etag = 'md5hashsum';
+ * this.response.etag = '"md5hashsum"';
+ * this.response.etag = 'W/"123456789"';
+ *
+ * @param {String} etag
+ * @api public
+ */
+ etag: string;
+
+ /**
+ * Set header `field` to `val`, or pass
+ * an object of header fields.
+ *
+ * Examples:
+ *
+ * this.set('Foo', ['bar', 'baz']);
+ * this.set('Accept', 'application/json');
+ * this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
+ */
+ set(field: { [key: string]: string | string[] }): void;
+ set(field: string, val: string | string[]): void;
+
+ /**
+ * Append additional header `field` with value `val`.
+ *
+ * Examples:
+ *
+ * ```
+ * this.append('Link', ['', '']);
+ * this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
+ * this.append('Warning', '199 Miscellaneous warning');
+ * ```
+ */
+ append(field: string, val: string | string[]): void;
+
+ /**
+ * Remove header `field`.
+ */
+ remove(field: string): void;
+
+ /**
+ * Checks if the request is writable.
+ * Tests for the existence of the socket
+ * as node sometimes does not set it.
+ */
+ writable: boolean;
+
+ /**
+ * Flush any set headers, and begin the body
+ */
+ flushHeaders(): void;
+}
+
+declare class Application<
+ StateT = Application.DefaultState,
+ ContextT = Application.DefaultContext,
+> extends EventEmitter {
+ proxy: boolean;
+ proxyIpHeader: string;
+ maxIpsCount: number;
+ middleware: Array>;
+ subdomainOffset: number;
+ env: string;
+ context: Application.BaseContext & ContextT;
+ request: Application.BaseRequest;
+ response: Application.BaseResponse;
+ silent: boolean;
+ keys: Keygrip | string[];
+ ctxStorage: AsyncLocalStorage | undefined;
+
+ /**
+ * @param {object} [options] Application options
+ * @param {string} [options.env='development'] Environment
+ * @param {string[]} [options.keys] Signed cookie keys
+ * @param {boolean} [options.proxy] Trust proxy headers
+ * @param {number} [options.subdomainOffset] Subdomain offset
+ * @param {string} [options.proxyIpHeader] Proxy IP header, defaults to X-Forwarded-For
+ * @param {number} [options.maxIpsCount] Max IPs read from proxy IP header, default to 0 (means infinity)
+ * @param {boolean} [options.asyncLocalStorage] Enable AsyncLocalStorage
+ */
+ constructor(options?: {
+ env?: string | undefined;
+ keys?: string[] | undefined;
+ proxy?: boolean | undefined;
+ subdomainOffset?: number | undefined;
+ proxyIpHeader?: string | undefined;
+ maxIpsCount?: number | undefined;
+ asyncLocalStorage?: boolean | undefined;
+ });
+
+ /**
+ * Shorthand for:
+ *
+ * http.createServer(app.callback()).listen(...)
+ */
+ listen(port?: number, hostname?: string, backlog?: number, listeningListener?: () => void): Server;
+ listen(port: number, hostname?: string, listeningListener?: () => void): Server;
+ listen(port: number, backlog?: number, listeningListener?: () => void): Server;
+ listen(port: number, listeningListener?: () => void): Server;
+ listen(path: string, backlog?: number, listeningListener?: () => void): Server;
+ listen(path: string, listeningListener?: () => void): Server;
+ listen(options: ListenOptions, listeningListener?: () => void): Server;
+ listen(handle: any, backlog?: number, listeningListener?: () => void): Server;
+ listen(handle: any, listeningListener?: () => void): Server;
+
+ /**
+ * Return JSON representation.
+ * We only bother showing settings.
+ */
+ inspect(): any;
+
+ /**
+ * Return JSON representation.
+ * We only bother showing settings.
+ */
+ toJSON(): any;
+
+ /**
+ * Use the given middleware `fn`.
+ *
+ * Old-style middleware will be converted.
+ */
+ use(
+ middleware: Application.Middleware,
+ ): Application;
+
+ /**
+ * Return a request handler callback
+ * for node's native http/http2 server.
+ */
+ callback(): (req: IncomingMessage | Http2ServerRequest, res: ServerResponse | Http2ServerResponse) => Promise;
+
+ /**
+ * Initialize a new context.
+ *
+ * @api private
+ */
+ createContext(
+ req: IncomingMessage,
+ res: ServerResponse,
+ ): Application.ParameterizedContext;
+
+ /**
+ * Default error handler.
+ *
+ * @api private
+ */
+ onerror(err: Error): void;
+
+ /**
+ * return current context from async local storage
+ */
+ readonly currentContext: ContextT | undefined;
+}
+
+declare namespace Application {
+ type DefaultStateExtends = any;
+ /**
+ * This interface can be augmented by users to add types to Koa's default state
+ */
+ interface DefaultState extends DefaultStateExtends {}
+
+ type DefaultContextExtends = {};
+ /**
+ * This interface can be augmented by users to add types to Koa's default context
+ */
+ interface DefaultContext extends DefaultContextExtends {
+ /**
+ * Custom properties.
+ */
+ [key: PropertyKey]: any;
+ }
+
+ type Middleware = compose.Middleware<
+ ParameterizedContext
+ >;
+
+ interface BaseRequest extends ContextDelegatedRequest {
+ /**
+ * Get the charset when present or undefined.
+ */
+ charset: string;
+
+ /**
+ * Return parsed Content-Length when present.
+ */
+ length: number;
+
+ /**
+ * Return the request mime type void of
+ * parameters such as "charset".
+ */
+ type: string;
+
+ /**
+ * Inspect implementation.
+ */
+ inspect(): any;
+
+ /**
+ * Return JSON representation.
+ */
+ toJSON(): any;
+ }
+
+ interface BaseResponse extends ContextDelegatedResponse {
+ /**
+ * Return the request socket.
+ *
+ * @return {Connection}
+ * @api public
+ */
+ socket: Socket;
+
+ /**
+ * Return response header.
+ */
+ header: OutgoingHttpHeaders;
+
+ /**
+ * Return response header, alias as response.header
+ */
+ headers: OutgoingHttpHeaders;
+
+ /**
+ * Check whether the response is one of the listed types.
+ * Pretty much the same as `this.request.is()`.
+ *
+ * @param {String|Array} types...
+ * @return {String|false}
+ * @api public
+ */
+ // is(): string;
+ is(...types: string[]): string | false | null;
+ is(types: string[]): string | false | null;
+
+ /**
+ * Return response header. If the header is not set, will return an empty
+ * string.
+ *
+ * The `Referrer` header field is special-cased, both `Referrer` and
+ * `Referer` are interchangeable.
+ *
+ * Examples:
+ *
+ * this.get('Content-Type');
+ * // => "text/plain"
+ *
+ * this.get('content-type');
+ * // => "text/plain"
+ *
+ * this.get('Something');
+ * // => ''
+ */
+ get(field: string): string;
+
+ /**
+ * Inspect implementation.
+ */
+ inspect(): any;
+
+ /**
+ * Return JSON representation.
+ */
+ toJSON(): any;
+ }
+
+ interface BaseContext extends ContextDelegatedRequest, ContextDelegatedResponse {
+ /**
+ * util.inspect() implementation, which
+ * just returns the JSON output.
+ */
+ inspect(): any;
+
+ /**
+ * Return JSON representation.
+ *
+ * Here we explicitly invoke .toJSON() on each
+ * object, as iteration will otherwise fail due
+ * to the getters and cause utilities such as
+ * clone() to fail.
+ */
+ toJSON(): any;
+
+ /**
+ * Similar to .throw(), adds assertion.
+ *
+ * this.assert(this.user, 401, 'Please login!');
+ *
+ * See: https://github.com/jshttp/http-assert
+ */
+ assert: typeof httpAssert;
+
+ /**
+ * Throw an error with `msg` and optional `status`
+ * defaulting to 500. Note that these are user-level
+ * errors, and the message may be exposed to the client.
+ *
+ * this.throw(403)
+ * this.throw('name required', 400)
+ * this.throw(400, 'name required')
+ * this.throw('something exploded')
+ * this.throw(new Error('invalid'), 400);
+ * this.throw(400, new Error('invalid'));
+ *
+ * See: https://github.com/jshttp/http-errors
+ */
+ throw(message: string, code?: number, properties?: {}): never;
+ throw(status: number): never;
+ throw(...properties: Array): never;
+
+ /**
+ * Default error handling.
+ */
+ onerror(err: Error): void;
+ }
+
+ interface Request extends BaseRequest {
+ app: Application;
+ req: IncomingMessage;
+ res: ServerResponse;
+ ctx: Context;
+ response: Response;
+ originalUrl: string;
+ ip: string;
+ accept: accepts.Accepts;
+ }
+
+ interface Response extends BaseResponse {
+ app: Application;
+ req: IncomingMessage;
+ res: ServerResponse;
+ ctx: Context;
+ request: Request;
+ }
+
+ interface ExtendableContext extends BaseContext {
+ app: Application;
+ request: Request;
+ response: Response;
+ req: IncomingMessage;
+ res: ServerResponse;
+ originalUrl: string;
+ cookies: Cookies;
+ accept: accepts.Accepts;
+ /**
+ * To bypass Koa's built-in response handling, you may explicitly set `ctx.respond = false;`
+ */
+ respond?: boolean | undefined;
+ }
+
+ type ParameterizedContext =
+ & ExtendableContext
+ & { state: StateT }
+ & ContextT
+ & { body: ResponseBodyT; response: { body: ResponseBodyT } };
+
+ interface Context extends ParameterizedContext {}
+
+ type Next = () => Promise;
+
+ /**
+ * A re-export of `HttpError` from the `http-error` package.
+ *
+ * This is the error type that is thrown by `ctx.assert()` and `ctx.throw()`.
+ */
+ const HttpError: typeof HttpErrors.HttpError;
+}
+
+export = Application;
diff --git a/types/koa/v2/package.json b/types/koa/v2/package.json
new file mode 100644
index 00000000000000..57dcf0a61596c3
--- /dev/null
+++ b/types/koa/v2/package.json
@@ -0,0 +1,51 @@
+{
+ "private": true,
+ "name": "@types/koa",
+ "version": "2.15.9999",
+ "projects": [
+ "http://koajs.com"
+ ],
+ "dependencies": {
+ "@types/accepts": "*",
+ "@types/content-disposition": "*",
+ "@types/cookies": "*",
+ "@types/http-assert": "*",
+ "@types/http-errors": "*",
+ "@types/keygrip": "*",
+ "@types/koa-compose": "*",
+ "@types/node": "*"
+ },
+ "devDependencies": {
+ "@types/koa": "workspace:."
+ },
+ "owners": [
+ {
+ "name": "jKey Lu",
+ "githubUsername": "jkeylu"
+ },
+ {
+ "name": "Brice Bernard",
+ "githubUsername": "brikou"
+ },
+ {
+ "name": "harryparkdotio",
+ "githubUsername": "harryparkdotio"
+ },
+ {
+ "name": "Wooram Jun",
+ "githubUsername": "chatoo2412"
+ },
+ {
+ "name": "Christian Vaagland Tellnes",
+ "githubUsername": "tellnes"
+ },
+ {
+ "name": "Piotr Kuczynski",
+ "githubUsername": "pkuczynski"
+ },
+ {
+ "name": "vnoder",
+ "githubUsername": "vnoder"
+ }
+ ]
+}
diff --git a/types/koa/v2/test/constructor.ts b/types/koa/v2/test/constructor.ts
new file mode 100644
index 00000000000000..eb3c7debc44b04
--- /dev/null
+++ b/types/koa/v2/test/constructor.ts
@@ -0,0 +1,19 @@
+import Koa = require("koa");
+
+const app = new Koa({
+ env: "abc",
+ keys: ["im a newer secret", "i like turtle"],
+ proxy: true,
+ subdomainOffset: 2,
+ proxyIpHeader: "XYZ-Forwarded-For",
+ maxIpsCount: 2,
+ asyncLocalStorage: true,
+});
+
+app.use(ctx => {
+ ctx.body = "Hello World";
+});
+
+app.listen(3000);
+
+const server = app.listen();
diff --git a/types/koa/v2/test/default.ts b/types/koa/v2/test/default.ts
new file mode 100644
index 00000000000000..f9e34b8297eb05
--- /dev/null
+++ b/types/koa/v2/test/default.ts
@@ -0,0 +1,78 @@
+import Koa = require("koa");
+
+declare module "koa" {
+ interface DefaultState {
+ stateProperty: boolean;
+ }
+
+ interface DefaultContext {
+ logger: {
+ info: Function;
+ log: Function;
+ error: Function;
+ };
+ }
+}
+
+const app = new Koa();
+
+app.context.logger = {
+ info: () => {},
+ log: () => {},
+ error: () => {},
+};
+
+app.use((ctx, next) => {
+ ctx.state.stateProperty = false;
+ return next();
+});
+
+app.use<{ a: boolean }>(async (ctx, next) => {
+ ctx.state.a = true;
+ ctx.state.b = ""; // undeclared property
+ await next();
+});
+
+app.use((ctx: Koa.Context, next) => {
+ const start: any = new Date();
+ return next().then(() => {
+ const end: any = new Date();
+ const ms = end - start;
+ ctx.logger.info(`${ctx.method} ${ctx.url} - ${ms}ms`);
+ ctx.user = {}; // undeclared property
+ ctx["views"] = 123; // string property key
+ ctx["views"]; // $ExpectType any
+ ctx[200] = {}; // number property key
+ ctx[200]; // $ExpectType any
+ ctx[Symbol("locale")] = "en-US"; // symbol property key
+ ctx[Symbol("locale")]; // $ExpectType any
+ ctx.assert(true, 404, "Yep!");
+ });
+});
+
+// response
+app.use(ctx => {
+ ctx.body = "Hello World";
+ ctx.body = ctx.URL.toString();
+ ctx.set({
+ link: ["", ""],
+ });
+ ctx.attachment();
+ ctx.attachment("path/to/tobi.png");
+ ctx.attachment("path/to/tobi.png", {
+ type: "inline",
+ });
+});
+
+app.on("error", error => {
+ if (error instanceof Koa.HttpError) {
+ // $ExpectType number
+ error.status;
+ throw error;
+ }
+ throw error;
+});
+
+app.listen(3000);
+
+const server = app.listen();
diff --git a/types/koa/v2/test/index.ts b/types/koa/v2/test/index.ts
new file mode 100644
index 00000000000000..5e64ec35b50455
--- /dev/null
+++ b/types/koa/v2/test/index.ts
@@ -0,0 +1,88 @@
+import Koa = require("koa");
+
+declare module "koa" {
+ interface ExtendableContext {
+ errors?: Error[] | undefined;
+ }
+}
+
+interface DbBaseContext {
+ db(): void;
+}
+
+interface UserContext {
+ user: {};
+}
+
+const app = new Koa<{}, DbBaseContext>();
+
+app.context.db = () => {};
+
+app.use(async ctx => {
+ if (ctx.errors) {
+ ctx.throw(400, ctx.errors[0]);
+ ctx.throw(400);
+ ctx.throw(400, "name required");
+ ctx.throw(400, "name required", { user: { id: 7 } });
+ ctx.throw(403);
+ ctx.throw(400, "name required");
+ ctx.throw("something exploded");
+ ctx.throw(new Error("invalid"));
+ ctx.throw(400, new Error("invalid"));
+ }
+});
+
+app.use(async (ctx, next) => {
+ try {
+ return await next();
+ } catch (ex: any) {
+ ctx.errors = [ex];
+ }
+});
+
+app.use<{}, UserContext>(async ctx => {
+ console.log(ctx.db);
+ ctx.user = {};
+});
+
+app.use((ctx: Koa.Context, next) => {
+ const start: any = new Date();
+ return next().then(() => {
+ const end: any = new Date();
+ const ms = end - start;
+ console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
+ ctx.assert(true, 404, "Yep!");
+ });
+});
+
+app.use(ctx => {
+ ctx.accepts(); // $ExpectType string[]
+ ctx.accepts(""); // $ExpectType string | false
+ ctx.accepts([""]); // $ExpectType string | false
+ ctx.acceptsEncodings(); // $ExpectType string[]
+ ctx.acceptsEncodings(""); // $ExpectType string | false
+ ctx.acceptsEncodings([""]); // $ExpectType string | false
+ ctx.acceptsCharsets(); // $ExpectType string[]
+ ctx.acceptsCharsets(""); // $ExpectType string | false
+ ctx.acceptsCharsets([""]); // $ExpectType string | false
+ ctx.acceptsLanguages(); // $ExpectType string[]
+ ctx.acceptsLanguages(""); // $ExpectType string | false
+ ctx.acceptsLanguages([""]); // $ExpectType string | false
+ ctx.is(""); // $ExpectType string | false | null
+ ctx.is([""]); // $ExpectType string | false | null
+
+ ctx.vary("Origin"); // $ExpectType void
+ ctx.vary(["Origin", "User-Agent"]); // $ExpectType void
+});
+
+// response
+app.use(ctx => {
+ ctx.body = "Hello World";
+ ctx.body = ctx.URL.toString();
+});
+
+app.listen(3000);
+
+const currentContext: DbBaseContext | undefined = app.currentContext;
+
+const server = app.listen();
diff --git a/types/koa/v2/test/settings.ts b/types/koa/v2/test/settings.ts
new file mode 100644
index 00000000000000..55eb07390779f3
--- /dev/null
+++ b/types/koa/v2/test/settings.ts
@@ -0,0 +1,25 @@
+// Tests for application settings. https://github.com/koajs/koa/blob/master/docs/api/index.md#settings
+
+import Koa = require("koa");
+
+const app = new Koa();
+
+app.env = "development";
+
+app.keys = ["im a newer secret", "i like turtle"];
+
+app.proxy = true;
+
+app.subdomainOffset = 2;
+
+app.proxyIpHeader = "X-Forwarded-For";
+
+app.maxIpsCount = 0;
+
+app.use(ctx => {
+ ctx.body = "Hello World";
+});
+
+app.listen(3000);
+
+const server = app.listen();
diff --git a/types/koa/v2/test/typed-response-body.ts b/types/koa/v2/test/typed-response-body.ts
new file mode 100644
index 00000000000000..05e5b77abff9dd
--- /dev/null
+++ b/types/koa/v2/test/typed-response-body.ts
@@ -0,0 +1,28 @@
+import Koa = require("koa");
+
+const app = new Koa();
+
+interface CustomResponseBody {
+ a: number;
+ b: string;
+}
+
+const middleware: Koa.Middleware = (ctx, next) => {
+ ctx.body.a = 1;
+ ctx.body.b = "text";
+
+ ctx.response.body = {
+ a: 1,
+ b: "text",
+ // @ts-expect-error
+ c: true,
+ };
+
+ return next();
+};
+
+app.use(middleware);
+
+app.listen(3000);
+
+const server = app.listen();
diff --git a/types/koa/v2/tsconfig.json b/types/koa/v2/tsconfig.json
new file mode 100644
index 00000000000000..aa8f83fee67449
--- /dev/null
+++ b/types/koa/v2/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "files": [
+ "index.d.ts",
+ "test/index.ts",
+ "test/constructor.ts",
+ "test/default.ts",
+ "test/settings.ts",
+ "test/typed-response-body.ts"
+ ],
+ "compilerOptions": {
+ "module": "node16",
+ "lib": [
+ "es6"
+ ],
+ "noImplicitAny": true,
+ "noImplicitThis": true,
+ "strictNullChecks": true,
+ "strictFunctionTypes": true,
+ "types": [],
+ "noEmit": true,
+ "forceConsistentCasingInFileNames": true
+ }
+}