feat(api): added multiple things. see below
- Added ratelimiting - Rewrote api handling and take advantage of bucket hash - Removed the word "api" in api client options BREAKING CHANGE: Removed the word "api" in client options
This commit is contained in:
parent
8f2abedef0
commit
eb5c3e0090
10 changed files with 220 additions and 59 deletions
|
@ -66,6 +66,12 @@
|
||||||
"always"
|
"always"
|
||||||
],
|
],
|
||||||
"no-var": "error",
|
"no-var": "error",
|
||||||
|
"eqeqeq": "error",
|
||||||
|
"no-eq-null": "error",
|
||||||
|
"arrow-parens": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
// TypeScript Rules
|
// TypeScript Rules
|
||||||
"@typescript-eslint/naming-convention": [
|
"@typescript-eslint/naming-convention": [
|
||||||
"error",
|
"error",
|
||||||
|
@ -127,8 +133,8 @@
|
||||||
"builtin",
|
"builtin",
|
||||||
"external",
|
"external",
|
||||||
"internal",
|
"internal",
|
||||||
"sibling",
|
|
||||||
"parent",
|
"parent",
|
||||||
|
"sibling",
|
||||||
"index",
|
"index",
|
||||||
"object",
|
"object",
|
||||||
"type"
|
"type"
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { ApiHandler } from './apiHandler';
|
import { ApiManager } from './apiManager';
|
||||||
|
|
||||||
import type { Client } from '../client/client';
|
import type { Client } from '../client/client';
|
||||||
import type { IRequestOptions } from '../utils/types';
|
import type { IRequestOptions } from '../utils/types';
|
||||||
|
|
||||||
export class ApiClient {
|
export class ApiClient {
|
||||||
public client: Client;
|
public client: Client;
|
||||||
public handler: ApiHandler;
|
public manager: ApiManager;
|
||||||
|
|
||||||
private _token: string;
|
private _token: string;
|
||||||
|
|
||||||
|
@ -13,11 +13,11 @@ export class ApiClient {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this._token = token;
|
this._token = token;
|
||||||
|
|
||||||
this.handler = new ApiHandler(this.client, this._token);
|
this.manager = new ApiManager(this.client, this._token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async delete<T>(options: IRequestOptions): Promise<T | null> {
|
public async delete<T>(options: IRequestOptions): Promise<T> {
|
||||||
return this.handler.makeRequest<T>({
|
return this.manager.createRequest<T>({
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
body: options.body,
|
body: options.body,
|
||||||
headers: options.headers,
|
headers: options.headers,
|
||||||
|
@ -28,8 +28,8 @@ export class ApiClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get<T>(options: IRequestOptions): Promise<T | null> {
|
public async get<T>(options: IRequestOptions): Promise<T> {
|
||||||
return this.handler.makeRequest<T>({
|
return this.manager.createRequest<T>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
body: options.body,
|
body: options.body,
|
||||||
headers: options.headers,
|
headers: options.headers,
|
||||||
|
@ -40,8 +40,8 @@ export class ApiClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async patch<T>(options: IRequestOptions): Promise<T | null> {
|
public async patch<T>(options: IRequestOptions): Promise<T> {
|
||||||
return this.handler.makeRequest<T>({
|
return this.manager.createRequest<T>({
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: options.body,
|
body: options.body,
|
||||||
headers: options.headers,
|
headers: options.headers,
|
||||||
|
@ -52,8 +52,8 @@ export class ApiClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async post<T>(options: IRequestOptions): Promise<T | null> {
|
public async post<T>(options: IRequestOptions): Promise<T> {
|
||||||
return this.handler.makeRequest<T>({
|
return this.manager.createRequest<T>({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: options.body,
|
body: options.body,
|
||||||
headers: options.headers,
|
headers: options.headers,
|
||||||
|
@ -64,8 +64,8 @@ export class ApiClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async put<T>(options: IRequestOptions): Promise<T | null> {
|
public async put<T>(options: IRequestOptions): Promise<T> {
|
||||||
return this.handler.makeRequest<T>({
|
return this.manager.createRequest<T>({
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: options.body,
|
body: options.body,
|
||||||
headers: options.headers,
|
headers: options.headers,
|
||||||
|
|
|
@ -3,68 +3,134 @@ import { queue, QueueObject } from 'async';
|
||||||
import FormData from 'form-data';
|
import FormData from 'form-data';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
|
|
||||||
|
import type { ApiManager } from './apiManager';
|
||||||
import type { Client } from '../client/client';
|
import type { Client } from '../client/client';
|
||||||
import type { IMakeRequestOptions } from '../utils/types';
|
import type { IMakeRequestOptions, IRequestOptions, IRouteIdentifier } from '../utils/types';
|
||||||
|
import { sleep } from '../utils/sleep';
|
||||||
|
|
||||||
export class ApiHandler {
|
export class ApiHandler {
|
||||||
public client: Client;
|
public client: Client;
|
||||||
|
public hash: string;
|
||||||
|
public id: string;
|
||||||
|
public limit: number;
|
||||||
|
public majorParameter: string;
|
||||||
|
public manager: ApiManager;
|
||||||
public queue: QueueObject<unknown>;
|
public queue: QueueObject<unknown>;
|
||||||
|
public remaining: number;
|
||||||
|
public reset: number;
|
||||||
|
|
||||||
private _token: string;
|
private _token: string;
|
||||||
|
|
||||||
public constructor(client: Client, token: string) {
|
public constructor(client: Client, manager: ApiManager, token: string, hash: string, majorParameter: string) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this._token = token;
|
this._token = token;
|
||||||
|
|
||||||
this.queue = queue(async (requestOptions: IMakeRequestOptions, callback) => {
|
this.hash = hash;
|
||||||
callback(await this.makeRequest(requestOptions));
|
this.id = `${hash}:${majorParameter}`;
|
||||||
|
this.limit = -1;
|
||||||
|
this.majorParameter = majorParameter;
|
||||||
|
this.manager = manager;
|
||||||
|
this.queue = queue(async (task: { routeId: IRouteIdentifier, requestOptions: IMakeRequestOptions; }, callback) => {
|
||||||
|
if (this.limited) await sleep(this.timeToReset);
|
||||||
|
callback(await this.makeRequest(task.routeId, task.requestOptions));
|
||||||
});
|
});
|
||||||
|
this.remaining = -1;
|
||||||
|
this.reset = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get baseApiUrl(): string {
|
public async makeRequest<T>(routeId: IRouteIdentifier, options: IMakeRequestOptions, retries = 0): Promise<T> {
|
||||||
return `${this.client.options.api.apiUrl}/v${this.client.options.api.apiVersion}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async makeRequest<T>(options: IMakeRequestOptions): Promise<T | null> {
|
|
||||||
const headers: Map<string, string> = new Map();
|
const headers: Map<string, string> = new Map();
|
||||||
|
|
||||||
if (options.headers) for (const prop in options.headers) headers.set(prop, options.headers[prop]!);
|
if (options.headers) for (const prop in options.headers) headers.set(prop, options.headers[prop]!);
|
||||||
|
|
||||||
if (options.requireAuth) headers.set('Authorization', `Bot ${this._token}`);
|
|
||||||
if (options.reason) headers.set('X-Audit-Log-Reason', encodeURIComponent(options.reason));
|
if (options.reason) headers.set('X-Audit-Log-Reason', encodeURIComponent(options.reason));
|
||||||
|
if (options.requireAuth) headers.set('Authorization', `Bot ${this._token}`);
|
||||||
|
|
||||||
let body: FormData | string;
|
let body: FormData | string;
|
||||||
if (options.files && options.files.length) {
|
if (options.files && options.files.length) {
|
||||||
body = new FormData();
|
body = new FormData();
|
||||||
for (const file of options.files) if (file && file.file) body.append(file.name, file.file, file.name);
|
|
||||||
if (options.body) body.append('payload_json', JSON.stringify(options.body));
|
if (options.body) body.append('payload_json', JSON.stringify(options.body));
|
||||||
|
|
||||||
|
for (const file of options.files) if (file && file.file) body.append(file.name, file.file, file.name);
|
||||||
} else if (options.body) {
|
} else if (options.body) {
|
||||||
body = JSON.stringify(options.body);
|
body = JSON.stringify(options.body);
|
||||||
headers.set('Content-Type', 'application/json');
|
headers.set('Content-Type', 'application/json');
|
||||||
}
|
}
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), this.client.options.api.apiRequestTimeout);
|
const timeout = setTimeout(() => controller.abort(), this.client.options.api.requestTimeout);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const res = await fetch(`${this.baseApiUrl}${options.path}`, { method: options.method, headers, signal: controller.signal, body });
|
const res = await fetch(`${this.baseApiUrl}${options.path}`, { method: options.method, headers, signal: controller.signal, body });
|
||||||
|
|
||||||
// TODO: handle Ratelimits
|
const bucketHash = res.headers.get('x-ratelimit-bucket');
|
||||||
|
const globalRatelimit = res.headers.get('x-ratelimit-global');
|
||||||
|
const limit = res.headers.get('x-ratelimit-limit');
|
||||||
|
const remaining = res.headers.get('x-ratelimit-remaining');
|
||||||
|
const reset = res.headers.get('x-ratelimit-reset');
|
||||||
|
const resetAfter = res.headers.get('x-ratelimit-reset-after');
|
||||||
|
const retry = res.headers.get('retry-after');
|
||||||
|
const serverDate = res.headers.get('date');
|
||||||
|
|
||||||
|
if (!resetAfter && options.path.includes('reactions')) {
|
||||||
|
this.reset = new Date(Number(serverDate)).getTime() - (new Date(Number(serverDate)).getTime() - Date.now()) + 250;
|
||||||
|
}
|
||||||
|
|
||||||
|
let retryAfter = 0;
|
||||||
|
|
||||||
|
this.limit = limit !== null ? Number(limit) : Number.MAX_VALUE;
|
||||||
|
this.remaining = remaining !== null ? Number(remaining) : Number.MAX_VALUE;
|
||||||
|
this.reset = reset || resetAfter
|
||||||
|
? Number(reset) * 1000 + Date.now() + this.client.options.api.offset
|
||||||
|
: Date.now();
|
||||||
|
|
||||||
|
if (retry !== null)
|
||||||
|
retryAfter = retry ? Number(retry) * 1000 : -1;
|
||||||
|
|
||||||
|
if (bucketHash !== null && bucketHash !== this.hash) {
|
||||||
|
if (!this.manager.hashes.has(`${options.method}-${routeId.route}`))
|
||||||
|
this.manager.hashes.set(`${options.method}-${routeId.route}`, bucketHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (globalRatelimit) {
|
||||||
|
this.manager.globalTimeout = true;
|
||||||
|
this.manager.globalReset = Date.now() + retryAfter;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Handle non-2xx responses (Will slowly add them as i get them)
|
// TODO: Handle non-2xx responses (Will slowly add them as i get them)
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
return res.json() as unknown as T;
|
return res.json() as unknown as T;
|
||||||
|
} else if (res.status === 429) {
|
||||||
|
await sleep(retryAfter);
|
||||||
|
return this.makeRequest(routeId, options, retries++);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
throw new Error('An unknown status code was returned!');
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async push<T>(requestOptions: IMakeRequestOptions): Promise<T> {
|
public async push<T>(routeId: IRouteIdentifier, requestOptions: IRequestOptions): Promise<T> {
|
||||||
return await this.queue.pushAsync<T>(requestOptions);
|
return await this.queue.pushAsync<T>({ routeId, requestOptions });
|
||||||
|
}
|
||||||
|
|
||||||
|
public get baseApiUrl(): string {
|
||||||
|
return `${this.client.options.api.url}/v${this.client.options.api.version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get inactive(): boolean {
|
||||||
|
return this.queue.length() === 0 && !this.limited;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get limited(): boolean {
|
||||||
|
if (typeof this.manager.globalTimeout === 'number') return this.manager.globalTimeout <= 0 && Date.now() < this.manager.globalReset;
|
||||||
|
else return this.remaining <= 0 && Date.now() < this.reset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get timeToReset(): number {
|
||||||
|
return this.reset - Date.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
64
src/api/apiManager.ts
Normal file
64
src/api/apiManager.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { ApiHandler } from './apiHandler';
|
||||||
|
import { Snowflake } from '../utils/snowflake';
|
||||||
|
|
||||||
|
import type { Client } from '../client/client';
|
||||||
|
import type { ApiMethods, IMakeRequestOptions, IRouteIdentifier } from '../utils/types';
|
||||||
|
|
||||||
|
export class ApiManager {
|
||||||
|
public client: Client;
|
||||||
|
public globalReset: number;
|
||||||
|
public globalTimeout: boolean | null;
|
||||||
|
public hashes: Map<string, string>;
|
||||||
|
public queues: Map<string, ApiHandler>;
|
||||||
|
|
||||||
|
private _token: string;
|
||||||
|
|
||||||
|
public constructor(client: Client, token: string) {
|
||||||
|
this.client = client;
|
||||||
|
this._token = token;
|
||||||
|
|
||||||
|
this.globalReset = 0;
|
||||||
|
this.globalTimeout = null;
|
||||||
|
this.hashes = new Map();
|
||||||
|
this.queues = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createRequest<T>(requestOptions: IMakeRequestOptions): Promise<T> {
|
||||||
|
const routeId = this.generateRouteIdentifiers(requestOptions.path, requestOptions.method);
|
||||||
|
let hash = this.hashes.get(`${requestOptions.method}-${routeId.route}`);
|
||||||
|
|
||||||
|
if (!hash) hash = `UnknownHash(${routeId.route})`;
|
||||||
|
|
||||||
|
let queue = this.queues.get(`${hash}:${routeId.majorParameter}`);
|
||||||
|
|
||||||
|
if (queue === null) {
|
||||||
|
const apiHandler = new ApiHandler(this.client, this, this._token, hash, routeId.majorParameter);
|
||||||
|
this.queues.set(apiHandler.id, apiHandler);
|
||||||
|
|
||||||
|
queue = this.queues.get(`${hash}:${routeId.majorParameter}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await queue!.push<T>(routeId, requestOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public generateRouteIdentifiers(endpoint: string, method: ApiMethods): IRouteIdentifier {
|
||||||
|
const result = new RegExp('^/(?:channels|guilds|webhooks)/(d{16,19})').exec(endpoint);
|
||||||
|
const majorParameter = result && result.length !== 0 ? result[1]!.toString() : 'global';
|
||||||
|
const baseRoute = endpoint.replace(new RegExp('d{16,19}'), ':id');
|
||||||
|
|
||||||
|
let exceptions = '';
|
||||||
|
|
||||||
|
if (method === 'DELETE' && baseRoute === '/channels/:id/messages/:id') {
|
||||||
|
const id = new RegExp('d{16,19}$').exec(endpoint);
|
||||||
|
const snowflake = Snowflake.fromSnowflake(id![0]!);
|
||||||
|
|
||||||
|
if (Date.now() - (new Date(snowflake).getTime() / 1000) > 1000 * 60 * 60 * 24 * 14)
|
||||||
|
exceptions += '[Delete Old Message]';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
route: `${baseRoute}${exceptions}`,
|
||||||
|
majorParameter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,4 +12,3 @@ export {
|
||||||
IMakeRequestOptions,
|
IMakeRequestOptions,
|
||||||
Primitives,
|
Primitives,
|
||||||
} from './utils/types';
|
} from './utils/types';
|
||||||
export { } from './utils/utils';
|
|
||||||
|
|
|
@ -3,9 +3,12 @@ import type { IDefaultOptions } from './types';
|
||||||
export const defaults: IDefaultOptions = {
|
export const defaults: IDefaultOptions = {
|
||||||
clientOptions: {
|
clientOptions: {
|
||||||
api: {
|
api: {
|
||||||
apiRequestTimeout: 5000,
|
offset: 500,
|
||||||
apiUrl: 'https://discord.com/api',
|
requestTimeout: 5000,
|
||||||
apiVersion: 9,
|
retryLimit: 5,
|
||||||
|
sweepInterval: 60000,
|
||||||
|
url: 'https://discord.com/api',
|
||||||
|
version: 9,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
5
src/utils/sleep.ts
Normal file
5
src/utils/sleep.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export const sleep = async (ms: number): Promise<void> => {
|
||||||
|
return new Promise((res) => {
|
||||||
|
setTimeout(() => res(), ms);
|
||||||
|
});
|
||||||
|
};
|
9
src/utils/snowflake.ts
Normal file
9
src/utils/snowflake.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export class Snowflake {
|
||||||
|
public static fromSnowflake(value: string): number {
|
||||||
|
return new Date((Number(value) >> 22) + 1420070400000).getTime() / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static toSnowflake(value: number): string {
|
||||||
|
return (((new Date(value).getTime() / 1000) - 1420070400000) << 22).toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,11 @@
|
||||||
// Interfaces
|
// Interfaces
|
||||||
export interface IApiClientOptions {
|
export interface IApiClientOptions {
|
||||||
apiUrl?: string;
|
offset?: number;
|
||||||
apiRequestTimeout?: number;
|
requestTimeout?: number;
|
||||||
apiVersion?: number;
|
retryLimit?: number;
|
||||||
|
sweepInterval?: number;
|
||||||
|
url?: string;
|
||||||
|
version?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IClientOptions {
|
export interface IClientOptions {
|
||||||
|
@ -23,48 +26,54 @@ export interface IMakeRequestOptions extends IRequestOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMessageEmbed {
|
export interface IMessageEmbed {
|
||||||
description: string;
|
|
||||||
title?: string;
|
|
||||||
type?: 'rich';
|
|
||||||
url?: string;
|
|
||||||
color?: number;
|
|
||||||
timestamp?: number;
|
|
||||||
author?: { name?: string, url?: string };
|
author?: { name?: string, url?: string };
|
||||||
fields?: { name: string, value: string, inline?: boolean }[];
|
color?: number;
|
||||||
footer?: { text?: string, icon_url?: string };
|
description?: string;
|
||||||
|
fields?: { inline?: boolean, name: string, value: string }[];
|
||||||
|
footer?: { icon_url?: string, text?: string };
|
||||||
image?: { url?: string };
|
image?: { url?: string };
|
||||||
provider?: { name?: string, url?: string };
|
provider?: { name?: string, url?: string };
|
||||||
|
timestamp?: number;
|
||||||
|
title?: string;
|
||||||
thumbnail?: { url?: string };
|
thumbnail?: { url?: string };
|
||||||
|
type?: 'rich';
|
||||||
|
url?: string;
|
||||||
video?: { url?: string };
|
video?: { url?: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRequestOptions {
|
export interface IRequestOptions {
|
||||||
|
body?: unknown;
|
||||||
|
files?: IFile[];
|
||||||
|
headers?: Record<string, string>;
|
||||||
path: string;
|
path: string;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
requireAuth?: boolean;
|
requireAuth?: boolean;
|
||||||
body?: unknown;
|
}
|
||||||
headers?: Record<string, string>;
|
|
||||||
files?: IFile[];
|
export interface IRouteIdentifier {
|
||||||
|
majorParameter: string;
|
||||||
|
route: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Api Rest Request Interfaces
|
// Api Rest Request Interfaces
|
||||||
// TODO: Add message components
|
// TODO: Add message components
|
||||||
export interface IApiCreateMessage {
|
export interface IApiCreateMessage {
|
||||||
content?: string;
|
allowed_mentions?: {
|
||||||
tts?: boolean;
|
parse?: 'everyone' | 'roles' | 'users'[];
|
||||||
embeds?: IMessageEmbed[];
|
replied_user?: boolean;
|
||||||
allowed_mentions: {
|
|
||||||
roles?: string[];
|
roles?: string[];
|
||||||
users?: string[];
|
users?: string[];
|
||||||
replied_user?: boolean;
|
|
||||||
parse?: 'everyone' | 'roles' | 'users'[];
|
|
||||||
};
|
};
|
||||||
message_reference: {
|
content?: string;
|
||||||
|
embeds?: IMessageEmbed[];
|
||||||
|
files?: IFile[];
|
||||||
|
message_reference?: {
|
||||||
channel_id?: string;
|
channel_id?: string;
|
||||||
|
fail_if_not_exists?: boolean;
|
||||||
guild_id?: string;
|
guild_id?: string;
|
||||||
message_id?: string;
|
message_id?: string;
|
||||||
fail_if_not_exists?: boolean;
|
|
||||||
};
|
};
|
||||||
|
tts?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enums
|
// Enums
|
||||||
|
@ -72,7 +81,7 @@ export interface IApiCreateMessage {
|
||||||
// Type Aliases
|
// Type Aliases
|
||||||
export type ApiMethods = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT' ;
|
export type ApiMethods = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT' ;
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
export type Builtin = Primitives | Function | Date | Error | RegExp;
|
export type Builtin = Date | Error | Function | Primitives | RegExp;
|
||||||
export type DeepRequired<T> = T extends Builtin
|
export type DeepRequired<T> = T extends Builtin
|
||||||
? NonNullable<T>
|
? NonNullable<T>
|
||||||
: T extends Map<infer K, infer V>
|
: T extends Map<infer K, infer V>
|
||||||
|
@ -93,4 +102,4 @@ export type DeepRequired<T> = T extends Builtin
|
||||||
: T extends {}
|
: T extends {}
|
||||||
? { [K in keyof T]-?: DeepRequired<T[K]> }
|
? { [K in keyof T]-?: DeepRequired<T[K]> }
|
||||||
: NonNullable<T>;
|
: NonNullable<T>;
|
||||||
export type Primitives = string | number | boolean | bigint | symbol | undefined | null;
|
export type Primitives = bigint | boolean | null | number | string | symbol | undefined;
|
||||||
|
|
Reference in a new issue