diff --git a/.changeset/little-dogs-exercise.md b/.changeset/little-dogs-exercise.md new file mode 100644 index 000000000..d1da8c6bf --- /dev/null +++ b/.changeset/little-dogs-exercise.md @@ -0,0 +1,5 @@ +--- +"uploadthing": patch +--- + +fix: runtime return type of `utapi.uploadFiles` diff --git a/packages/uploadthing/src/sdk/index.ts b/packages/uploadthing/src/sdk/index.ts index eb2f53456..86e75034b 100644 --- a/packages/uploadthing/src/sdk/index.ts +++ b/packages/uploadthing/src/sdk/index.ts @@ -1,8 +1,8 @@ import type { Json } from "@uploadthing/shared"; -import { UploadThingError, generateUploadThingURL } from "@uploadthing/shared"; +import { generateUploadThingURL, UploadThingError } from "@uploadthing/shared"; import { UPLOADTHING_VERSION } from "../constants"; -import type { FileEsque } from "./utils"; +import type { FileEsque, UploadData, UploadError } from "./utils"; import { uploadFilesInternal } from "./utils"; function guardServerOnly() { @@ -10,7 +10,7 @@ function guardServerOnly() { throw new UploadThingError({ code: "INTERNAL_SERVER_ERROR", message: "The `utapi` can only be used on the server.", - }) + }); } } @@ -19,13 +19,14 @@ function getApiKeyOrThrow() { throw new UploadThingError({ code: "MISSING_ENV", message: "Missing `UPLOADTHING_SECRET` env variable.", - }) + }); } return process.env.UPLOADTHING_SECRET; } -// File is just a Blob with a name property -type UploadFileResponse = Awaited>[0]; +type UploadFileResponse = + | { data: UploadData; error: null } + | { data: null; error: UploadError }; /** * @param {FileEsque | FileEsque[]} files The file(s) to upload @@ -43,9 +44,7 @@ type UploadFileResponse = Awaited>[0]; export const uploadFiles = async ( files: T, metadata: Json = {}, -): Promise< - T extends FileEsque[] ? UploadFileResponse[] : UploadFileResponse -> => { +) => { guardServerOnly(); const filesToUpload: FileEsque[] = Array.isArray(files) ? files : [files]; @@ -61,8 +60,11 @@ export const uploadFiles = async ( }, ); - // @ts-expect-error - ehh? type is tested in sdk.test.ts - return uploads; + const uploadFileResponse = Array.isArray(files) ? uploads : uploads[0]; + + return uploadFileResponse as T extends FileEsque[] + ? UploadFileResponse[] + : UploadFileResponse; }; /** @@ -82,7 +84,7 @@ type Url = string | URL; export const uploadFilesFromUrl = async ( urls: T, metadata: Json = {}, -): Promise => { +) => { guardServerOnly(); const fileUrls: Url[] = Array.isArray(urls) ? urls : [urls]; @@ -102,7 +104,7 @@ export const uploadFilesFromUrl = async ( code: "BAD_REQUEST", message: "Failed to download requested file.", cause: fileResponse, - }) + }); } const blob = await fileResponse.blob(); return Object.assign(blob, { name: filename }); @@ -120,8 +122,11 @@ export const uploadFilesFromUrl = async ( }, ); - // @ts-expect-error - ehh? type is tested in sdk.test.ts - return Array.isArray(urls) ? uploads : uploads[0]; + const uploadFileResponse = Array.isArray(urls) ? uploads : uploads[0]; + + return uploadFileResponse as T extends Url[] + ? UploadFileResponse[] + : UploadFileResponse; }; /** diff --git a/packages/uploadthing/src/sdk/sdk.test.ts b/packages/uploadthing/src/sdk/sdk.test.ts index 50b7dc246..77a572095 100644 --- a/packages/uploadthing/src/sdk/sdk.test.ts +++ b/packages/uploadthing/src/sdk/sdk.test.ts @@ -1,12 +1,8 @@ import { describe, expectTypeOf, test } from "vitest"; import * as utapi from "."; +import type { UploadError } from "./utils"; -type SerializedUploadthingError = { - code: string; - message: string; - data: any; -}; async function ignoreErrors(fn: () => T | Promise) { try { @@ -23,7 +19,7 @@ describe("uploadFiles", () => { expectTypeOf< ( | { data: { key: string; url: string }; error: null } - | { data: null; error: SerializedUploadthingError } + | { data: null; error: UploadError } )[] >(result); }); @@ -34,7 +30,7 @@ describe("uploadFiles", () => { const result = await utapi.uploadFiles({} as File); expectTypeOf< | { data: { key: string; url: string }; error: null } - | { data: null; error: SerializedUploadthingError } + | { data: null; error: UploadError } >(result); }); }); @@ -47,7 +43,7 @@ describe("uploadFilesFromUrl", () => { expectTypeOf< ( | { data: { key: string; url: string }; error: null } - | { data: null; error: SerializedUploadthingError } + | { data: null; error: UploadError } )[] >(result); }); @@ -58,7 +54,7 @@ describe("uploadFilesFromUrl", () => { const result = await utapi.uploadFilesFromUrl("foo"); expectTypeOf< | { data: { key: string; url: string }; error: null } - | { data: null; error: SerializedUploadthingError } + | { data: null; error: UploadError } >(result); }); }); diff --git a/packages/uploadthing/src/sdk/utils.ts b/packages/uploadthing/src/sdk/utils.ts index 4ff893392..00afa4aae 100644 --- a/packages/uploadthing/src/sdk/utils.ts +++ b/packages/uploadthing/src/sdk/utils.ts @@ -7,6 +7,17 @@ import { export type FileEsque = Blob & { name: string }; +export type UploadData = { + key: string; + url: string; +}; + +export type UploadError = { + code: string; + message: string; + data: any; +}; + export const uploadFilesInternal = async ( data: { files: FileEsque[]; @@ -100,11 +111,12 @@ export const uploadFilesInternal = async ( return uploads.map((upload) => { if (upload.status === "fulfilled") { - return { data: upload.value, error: null }; + const data = upload.value satisfies UploadData; + return { data, error: null }; } - return { - data: null, - error: UploadThingError.toObject(upload.reason as UploadThingError), - }; + // We only throw UploadThingErrors, so this is safe + const reason = upload.reason as UploadThingError; + const error = UploadThingError.toObject(reason) satisfies UploadError; + return { data: null, error }; }); };