forked from pinks/eris
exactOptionalPropertyTypes
This commit is contained in:
parent
f020499f4d
commit
4f2371fa8b
|
@ -1,7 +1,7 @@
|
|||
import { route } from "reroute";
|
||||
import { serveSpa } from "serve_spa";
|
||||
import { serveApi } from "./serveApi.ts";
|
||||
import { fromFileUrl } from "std/path/mod.ts"
|
||||
import { fromFileUrl } from "std/path/mod.ts";
|
||||
|
||||
export async function serveUi() {
|
||||
const server = Deno.serve({ port: 5999 }, (request) =>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { ulid } from "ulid";
|
|||
export const sessions = new Map<string, Session>();
|
||||
|
||||
export interface Session {
|
||||
userId?: number;
|
||||
userId?: number | undefined;
|
||||
}
|
||||
|
||||
export const sessionsRoute = createPathFilter({
|
||||
|
@ -24,7 +24,7 @@ export const sessionsRoute = createPathFilter({
|
|||
GET: createEndpoint(
|
||||
{ query: null, body: null },
|
||||
async ({ params }) => {
|
||||
const id = params.sessionId;
|
||||
const id = params.sessionId!;
|
||||
const session = sessions.get(id);
|
||||
if (!session) {
|
||||
return { status: 401, body: { type: "text/plain", data: "Session not found" } };
|
||||
|
|
|
@ -7,7 +7,7 @@ export const usersRoute = createPathFilter({
|
|||
GET: createEndpoint(
|
||||
{ query: null, body: null },
|
||||
async ({ params }) => {
|
||||
const chat = await bot.api.getChat(params.userId);
|
||||
const chat = await bot.api.getChat(params.userId!);
|
||||
if (chat.type !== "private") {
|
||||
throw new Error("Chat is not private");
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ export const usersRoute = createPathFilter({
|
|||
GET: createEndpoint(
|
||||
{ query: null, body: null },
|
||||
async ({ params }) => {
|
||||
const chat = await bot.api.getChat(params.userId);
|
||||
const chat = await bot.api.getChat(params.userId!);
|
||||
if (chat.type !== "private") {
|
||||
throw new Error("Chat is not private");
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
workerInstanceStore,
|
||||
} from "../app/workerInstanceStore.ts";
|
||||
import { getAuthHeader } from "../utils/getAuthHeader.ts";
|
||||
import { omitUndef } from "../utils/omitUndef.ts";
|
||||
import { withUser } from "./withUser.ts";
|
||||
|
||||
export type WorkerData = Omit<WorkerInstance, "sdUrl" | "sdAuth"> & {
|
||||
|
@ -105,7 +106,7 @@ export const workersRoute = createPathFilter({
|
|||
GET: createEndpoint(
|
||||
{ query: null, body: null },
|
||||
async ({ params }) => {
|
||||
const workerInstance = await workerInstanceStore.getById(params.workerId);
|
||||
const workerInstance = await workerInstanceStore.getById(params.workerId!);
|
||||
if (!workerInstance) {
|
||||
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
||||
}
|
||||
|
@ -126,7 +127,7 @@ export const workersRoute = createPathFilter({
|
|||
},
|
||||
},
|
||||
async ({ params, query, body }) => {
|
||||
const workerInstance = await workerInstanceStore.getById(params.workerId);
|
||||
const workerInstance = await workerInstanceStore.getById(params.workerId!);
|
||||
if (!workerInstance) {
|
||||
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
||||
}
|
||||
|
@ -136,7 +137,7 @@ export const workersRoute = createPathFilter({
|
|||
JSON.stringify(body.data)
|
||||
}`,
|
||||
);
|
||||
await workerInstance.update(body.data);
|
||||
await workerInstance.update(omitUndef(body.data));
|
||||
return {
|
||||
status: 200,
|
||||
body: { type: "application/json", data: await getWorkerData(workerInstance) },
|
||||
|
@ -152,7 +153,7 @@ export const workersRoute = createPathFilter({
|
|||
body: null,
|
||||
},
|
||||
async ({ params, query }) => {
|
||||
const workerInstance = await workerInstanceStore.getById(params.workerId);
|
||||
const workerInstance = await workerInstanceStore.getById(params.workerId!);
|
||||
if (!workerInstance) {
|
||||
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
||||
}
|
||||
|
@ -169,7 +170,7 @@ export const workersRoute = createPathFilter({
|
|||
GET: createEndpoint(
|
||||
{ query: null, body: null },
|
||||
async ({ params }) => {
|
||||
const workerInstance = await workerInstanceStore.getById(params.workerId);
|
||||
const workerInstance = await workerInstanceStore.getById(params.workerId!);
|
||||
if (!workerInstance) {
|
||||
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
||||
}
|
||||
|
@ -200,7 +201,7 @@ export const workersRoute = createPathFilter({
|
|||
GET: createEndpoint(
|
||||
{ query: null, body: null },
|
||||
async ({ params }) => {
|
||||
const workerInstance = await workerInstanceStore.getById(params.workerId);
|
||||
const workerInstance = await workerInstanceStore.getById(params.workerId!);
|
||||
if (!workerInstance) {
|
||||
return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { PngInfo } from "../bot/parsePngInfo.ts";
|
|||
import { formatOrdinal } from "../utils/formatOrdinal.ts";
|
||||
import { formatUserChat } from "../utils/formatUserChat.ts";
|
||||
import { getAuthHeader } from "../utils/getAuthHeader.ts";
|
||||
import { omitUndef } from "../utils/omitUndef.ts";
|
||||
import { SdError } from "./SdError.ts";
|
||||
import { getConfig } from "./config.ts";
|
||||
import { db, fs } from "./db.ts";
|
||||
|
@ -161,7 +162,7 @@ async function processGenerationJob(
|
|||
|
||||
// reduce size if worker can't handle the resolution
|
||||
const size = limitSize(
|
||||
{ ...config.defaultParams, ...state.task.params },
|
||||
omitUndef({ ...config.defaultParams, ...state.task.params }),
|
||||
1024 * 1024,
|
||||
);
|
||||
function limitSize(
|
||||
|
@ -182,18 +183,18 @@ async function processGenerationJob(
|
|||
// start generating the image
|
||||
const responsePromise = state.task.type === "txt2img"
|
||||
? workerSdClient.POST("/sdapi/v1/txt2img", {
|
||||
body: {
|
||||
body: omitUndef({
|
||||
...config.defaultParams,
|
||||
...state.task.params,
|
||||
...size,
|
||||
negative_prompt: state.task.params.negative_prompt
|
||||
? state.task.params.negative_prompt
|
||||
: config.defaultParams?.negative_prompt,
|
||||
},
|
||||
}),
|
||||
})
|
||||
: state.task.type === "img2img"
|
||||
? workerSdClient.POST("/sdapi/v1/img2img", {
|
||||
body: {
|
||||
body: omitUndef({
|
||||
...config.defaultParams,
|
||||
...state.task.params,
|
||||
...size,
|
||||
|
@ -209,7 +210,7 @@ async function processGenerationJob(
|
|||
).then((resp) => resp.arrayBuffer()),
|
||||
),
|
||||
],
|
||||
},
|
||||
}),
|
||||
})
|
||||
: undefined;
|
||||
|
||||
|
|
|
@ -5,10 +5,10 @@ import { db } from "./db.ts";
|
|||
export interface GenerationSchema {
|
||||
from: User;
|
||||
chat: Chat;
|
||||
sdInstanceId?: string; // TODO: change to workerInstanceKey
|
||||
info?: SdGenerationInfo;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
sdInstanceId?: string | undefined; // TODO: change to workerInstanceKey
|
||||
info?: SdGenerationInfo | undefined;
|
||||
startDate?: Date | undefined;
|
||||
endDate?: Date | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,19 +1,42 @@
|
|||
export interface KvMemoizeOptions<A extends Deno.KvKey, R> {
|
||||
/**
|
||||
* The time in milliseconds until the cached result expires.
|
||||
*/
|
||||
expireIn?: ((result: R, ...args: A) => number) | number | undefined;
|
||||
/**
|
||||
* Whether to recalculate the result if it was already cached.
|
||||
*
|
||||
* Runs whenever the result is retrieved from the cache.
|
||||
*/
|
||||
shouldRecalculate?: ((result: R, ...args: A) => boolean) | undefined;
|
||||
/**
|
||||
* Whether to cache the result after computing it.
|
||||
*
|
||||
* Runs whenever a new result is computed.
|
||||
*/
|
||||
shouldCache?: ((result: R, ...args: A) => boolean) | undefined;
|
||||
/**
|
||||
* Override the default KV store functions.
|
||||
*/
|
||||
override?: {
|
||||
set: (
|
||||
key: Deno.KvKey,
|
||||
args: A,
|
||||
value: R,
|
||||
options: { expireIn?: number },
|
||||
) => Promise<void>;
|
||||
get: (key: Deno.KvKey, args: A) => Promise<R | undefined>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Memoizes the function result in KV storage.
|
||||
* Memoizes the function result in KV store.
|
||||
*/
|
||||
export function kvMemoize<A extends Deno.KvKey, R>(
|
||||
db: Deno.Kv,
|
||||
key: Deno.KvKey,
|
||||
fn: (...args: A) => Promise<R>,
|
||||
options?: {
|
||||
expireIn?: number | ((result: R, ...args: A) => number);
|
||||
shouldRecalculate?: (result: R, ...args: A) => boolean;
|
||||
shouldCache?: (result: R, ...args: A) => boolean;
|
||||
override?: {
|
||||
set: (key: Deno.KvKey, args: A, value: R, options: { expireIn?: number }) => Promise<void>;
|
||||
get: (key: Deno.KvKey, args: A) => Promise<R | undefined>;
|
||||
};
|
||||
},
|
||||
options?: KvMemoizeOptions<A, R>,
|
||||
): (...args: A) => Promise<R> {
|
||||
return async (...args) => {
|
||||
const cachedResult = options?.override?.get
|
||||
|
@ -34,9 +57,9 @@ export function kvMemoize<A extends Deno.KvKey, R>(
|
|||
|
||||
if (options?.shouldCache?.(result, ...args) ?? (result != null)) {
|
||||
if (options?.override?.set) {
|
||||
await options.override.set(key, args, result, { expireIn });
|
||||
await options.override.set(key, args, result, expireIn != null ? { expireIn } : {});
|
||||
} else {
|
||||
await db.set([...key, ...args], result, { expireIn });
|
||||
await db.set([...key, ...args], result, expireIn != null ? { expireIn } : {});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -92,7 +92,7 @@ export async function processUploadQueue() {
|
|||
// send caption in separate message if it couldn't fit
|
||||
if (caption.text.length > 1024 && caption.text.length <= 4096) {
|
||||
await bot.api.sendMessage(state.chat.id, caption.text, {
|
||||
reply_to_message_id: resultMessages[0].message_id,
|
||||
reply_to_message_id: resultMessages[0]!.message_id,
|
||||
allow_sending_without_reply: true,
|
||||
entities: caption.entities,
|
||||
maxWait: 60,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { generationQueue } from "../app/generationQueue.ts";
|
||||
import { omitUndef } from "../utils/omitUndef.ts";
|
||||
import { ErisContext } from "./mod.ts";
|
||||
|
||||
export async function cancelCommand(ctx: ErisContext) {
|
||||
|
@ -7,8 +8,11 @@ export async function cancelCommand(ctx: ErisContext) {
|
|||
.filter((job) => job.lockUntil < new Date())
|
||||
.filter((j) => j.state.from.id === ctx.from?.id);
|
||||
for (const job of userJobs) await generationQueue.deleteJob(job.id);
|
||||
await ctx.reply(`Cancelled ${userJobs.length} jobs`, {
|
||||
await ctx.reply(
|
||||
`Cancelled ${userJobs.length} jobs`,
|
||||
omitUndef({
|
||||
reply_to_message_id: ctx.message?.message_id,
|
||||
allow_sending_without_reply: true,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
33
bot/mod.ts
33
bot/mod.ts
|
@ -6,6 +6,7 @@ import { error, info, warning } from "std/log/mod.ts";
|
|||
import { sessions } from "../api/sessionsRoute.ts";
|
||||
import { getConfig, setConfig } from "../app/config.ts";
|
||||
import { formatUserChat } from "../utils/formatUserChat.ts";
|
||||
import { omitUndef } from "../utils/omitUndef.ts";
|
||||
import { broadcastCommand } from "./broadcastCommand.ts";
|
||||
import { cancelCommand } from "./cancelCommand.ts";
|
||||
import { img2imgCommand, img2imgQuestion } from "./img2imgCommand.ts";
|
||||
|
@ -19,11 +20,11 @@ interface SessionData {
|
|||
}
|
||||
|
||||
interface ErisChatData {
|
||||
language?: string;
|
||||
language?: string | undefined;
|
||||
}
|
||||
|
||||
interface ErisUserData {
|
||||
params?: Record<string, string>;
|
||||
params?: Record<string, string> | undefined;
|
||||
}
|
||||
|
||||
export type ErisContext =
|
||||
|
@ -94,10 +95,13 @@ bot.use(async (ctx, next) => {
|
|||
await next();
|
||||
} catch (err) {
|
||||
try {
|
||||
await ctx.reply(`Handling update failed: ${err}`, {
|
||||
await ctx.reply(
|
||||
`Handling update failed: ${err}`,
|
||||
omitUndef({
|
||||
reply_to_message_id: ctx.message?.message_id,
|
||||
allow_sending_without_reply: true,
|
||||
});
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
throw err;
|
||||
}
|
||||
|
@ -122,24 +126,33 @@ bot.command("start", async (ctx) => {
|
|||
const id = ctx.match.trim();
|
||||
const session = sessions.get(id);
|
||||
if (session == null) {
|
||||
await ctx.reply("Login failed: Invalid session ID", {
|
||||
await ctx.reply(
|
||||
"Login failed: Invalid session ID",
|
||||
omitUndef({
|
||||
reply_to_message_id: ctx.message?.message_id,
|
||||
});
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
session.userId = ctx.from?.id;
|
||||
sessions.set(id, session);
|
||||
info(`User ${formatUserChat(ctx)} logged in`);
|
||||
// TODO: show link to web ui
|
||||
await ctx.reply("Login successful! You can now return to the WebUI.", {
|
||||
await ctx.reply(
|
||||
"Login successful! You can now return to the WebUI.",
|
||||
omitUndef({
|
||||
reply_to_message_id: ctx.message?.message_id,
|
||||
});
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.reply("Hello! Use the /txt2img command to generate an image", {
|
||||
await ctx.reply(
|
||||
"Hello! Use the /txt2img command to generate an image",
|
||||
omitUndef({
|
||||
reply_to_message_id: ctx.message?.message_id,
|
||||
});
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
bot.command("txt2img", txt2imgCommand);
|
||||
|
|
|
@ -25,7 +25,11 @@ interface PngInfoExtra extends PngInfo {
|
|||
upscale?: number;
|
||||
}
|
||||
|
||||
export function parsePngInfo(pngInfo: string, baseParams?: Partial<PngInfo>, shouldParseSeed?: boolean): Partial<PngInfo> {
|
||||
export function parsePngInfo(
|
||||
pngInfo: string,
|
||||
baseParams?: Partial<PngInfo>,
|
||||
shouldParseSeed?: boolean,
|
||||
): Partial<PngInfo> {
|
||||
const tags = pngInfo.split(/[,;]+|\.+\s|\n/u);
|
||||
let part: "prompt" | "negative_prompt" | "params" = "prompt";
|
||||
const params: Partial<PngInfoExtra> = {};
|
||||
|
@ -34,7 +38,7 @@ export function parsePngInfo(pngInfo: string, baseParams?: Partial<PngInfo>, sho
|
|||
for (const tag of tags) {
|
||||
const paramValuePair = tag.trim().match(/^(\w+\s*\w*):\s+(.*)$/u);
|
||||
if (paramValuePair) {
|
||||
const [, param, value] = paramValuePair;
|
||||
const [_match, param = "", value = ""] = paramValuePair;
|
||||
switch (param.replace(/\s+/u, "").toLowerCase()) {
|
||||
case "positiveprompt":
|
||||
case "positive":
|
||||
|
@ -67,7 +71,7 @@ export function parsePngInfo(pngInfo: string, baseParams?: Partial<PngInfo>, sho
|
|||
case "size":
|
||||
case "resolution": {
|
||||
part = "params";
|
||||
const [width, height] = value.trim()
|
||||
const [width = 0, height = 0] = value.trim()
|
||||
.split(/\s*[x,]\s*/u, 2)
|
||||
.map((v) => v.trim())
|
||||
.map(Number);
|
||||
|
@ -103,10 +107,12 @@ export function parsePngInfo(pngInfo: string, baseParams?: Partial<PngInfo>, sho
|
|||
part = "params";
|
||||
if (shouldParseSeed) {
|
||||
const seed = Number(value.trim());
|
||||
if (Number.isFinite(seed)) {
|
||||
params.seed = seed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "model":
|
||||
case "modelhash":
|
||||
case "modelname":
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { CommandContext } from "grammy";
|
||||
import { bold, fmt } from "grammy_parse_mode";
|
||||
import { StatelessQuestion } from "grammy_stateless_question";
|
||||
import { omitUndef } from "../utils/omitUndef.ts";
|
||||
import { ErisContext } from "./mod.ts";
|
||||
import { getPngInfo, parsePngInfo } from "./parsePngInfo.ts";
|
||||
|
||||
|
@ -23,11 +24,13 @@ async function pnginfo(ctx: ErisContext, includeRepliedTo: boolean): Promise<voi
|
|||
await ctx.reply(
|
||||
"Please send me a PNG file." +
|
||||
pnginfoQuestion.messageSuffixMarkdown(),
|
||||
omitUndef(
|
||||
{
|
||||
reply_markup: { force_reply: true, selective: true },
|
||||
parse_mode: "Markdown",
|
||||
reply_to_message_id: ctx.message?.message_id,
|
||||
},
|
||||
} as const,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -46,8 +49,11 @@ async function pnginfo(ctx: ErisContext, includeRepliedTo: boolean): Promise<voi
|
|||
params.width && params.height ? fmt`${bold("Size")}: ${params.width}x${params.height}` : "",
|
||||
]);
|
||||
|
||||
await ctx.reply(paramsText.text, {
|
||||
await ctx.reply(
|
||||
paramsText.text,
|
||||
omitUndef({
|
||||
entities: paramsText.entities,
|
||||
reply_to_message_id: ctx.message?.message_id,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
import { CommandContext } from "grammy";
|
||||
import { bold, fmt } from "grammy_parse_mode";
|
||||
import { activeGenerationWorkers, generationQueue } from "../app/generationQueue.ts";
|
||||
import { getFlagEmoji } from "../utils/getFlagEmoji.ts";
|
||||
import { ErisContext } from "./mod.ts";
|
||||
import { workerInstanceStore } from "../app/workerInstanceStore.ts";
|
||||
import { getFlagEmoji } from "../utils/getFlagEmoji.ts";
|
||||
import { omitUndef } from "../utils/omitUndef.ts";
|
||||
import { ErisContext } from "./mod.ts";
|
||||
|
||||
export async function queueCommand(ctx: CommandContext<ErisContext>) {
|
||||
let formattedMessage = await getMessageText();
|
||||
const queueMessage = await ctx.replyFmt(formattedMessage, {
|
||||
const queueMessage = await ctx.replyFmt(
|
||||
formattedMessage,
|
||||
omitUndef({
|
||||
disable_notification: true,
|
||||
reply_to_message_id: ctx.message?.message_id,
|
||||
});
|
||||
}),
|
||||
);
|
||||
handleFutureUpdates().catch(() => undefined);
|
||||
|
||||
async function getMessageText() {
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
"start": "deno run --unstable --allow-env --allow-read --allow-write --allow-net main.ts"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"jsx": "react"
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"jsx": "react",
|
||||
"noUncheckedIndexedAccess": true
|
||||
},
|
||||
"fmt": {
|
||||
"lineWidth": 100
|
||||
|
|
|
@ -34,7 +34,8 @@ export function AppHeader(
|
|||
);
|
||||
|
||||
const bot = useSWR(
|
||||
['bot',"GET",{}] as const, (args) => fetchApi(...args).then(handleResponse),
|
||||
["bot", "GET", {}] as const,
|
||||
(args) => fetchApi(...args).then(handleResponse),
|
||||
);
|
||||
|
||||
const userPhoto = useSWR(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { cx } from "@twind/core";
|
||||
import React from "react";
|
||||
|
||||
function CounterDigit(props: { value: number; transitionDurationMs?: number }) {
|
||||
function CounterDigit(props: { value: number; transitionDurationMs?: number | undefined }) {
|
||||
const { value, transitionDurationMs = 1500 } = props;
|
||||
const rads = -(Math.floor(value) % 1_000_000) * 2 * Math.PI * 0.1;
|
||||
|
||||
|
@ -36,10 +36,10 @@ const CounterText = (props: { children: React.ReactNode }) => (
|
|||
export function Counter(props: {
|
||||
value: number;
|
||||
digits: number;
|
||||
fractionDigits?: number;
|
||||
transitionDurationMs?: number;
|
||||
className?: string;
|
||||
postfix?: string;
|
||||
fractionDigits?: number | undefined;
|
||||
transitionDurationMs?: number | undefined;
|
||||
className?: string | undefined;
|
||||
postfix?: string | undefined;
|
||||
}) {
|
||||
const { value, digits, fractionDigits = 0, transitionDurationMs, className, postfix } = props;
|
||||
|
||||
|
|
|
@ -159,7 +159,7 @@ export function WorkersPage(props: { sessionId?: string }) {
|
|||
);
|
||||
}
|
||||
|
||||
function WorkerListItem(props: { worker: WorkerData; sessionId?: string }) {
|
||||
function WorkerListItem(props: { worker: WorkerData; sessionId: string | undefined }) {
|
||||
const { worker, sessionId } = props;
|
||||
const editWorkerModalRef = useRef<HTMLDialogElement>(null);
|
||||
const deleteWorkerModalRef = useRef<HTMLDialogElement>(null);
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { Chat, User } from "grammy_types";
|
||||
|
||||
export function formatUserChat(
|
||||
ctx: { from?: User; chat?: Chat; workerInstanceKey?: string },
|
||||
ctx: {
|
||||
from?: User | undefined;
|
||||
chat?: Chat | undefined;
|
||||
workerInstanceKey?: string | undefined;
|
||||
},
|
||||
) {
|
||||
const msg: string[] = [];
|
||||
if (ctx.from) {
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Removes all undefined properties from an object.
|
||||
*/
|
||||
export function omitUndef<O extends object | undefined>(object: O):
|
||||
& { [K in keyof O as undefined extends O[K] ? never : K]: O[K] }
|
||||
& { [K in keyof O as undefined extends O[K] ? K : never]?: O[K] & ({} | null) } {
|
||||
if (object == undefined) return object as never;
|
||||
return Object.fromEntries(
|
||||
Object.entries(object).filter(([, v]) => v !== undefined),
|
||||
) as never;
|
||||
}
|
Loading…
Reference in New Issue