Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Broken t.Files() and issues with t.File() when not returned alone #1023

Open
dy0gu opened this issue Jan 20, 2025 · 4 comments
Open

Broken t.Files() and issues with t.File() when not returned alone #1023

dy0gu opened this issue Jan 20, 2025 · 4 comments
Labels
bug Something isn't working

Comments

@dy0gu
Copy link

dy0gu commented Jan 20, 2025

What version of Elysia is running?

1.2.10

What platform is your computer?

Microsoft Windows NT 10.0.22631.0 x64

What steps can reproduce the bug?

Create some simple GET requests for files, like this:

// ROUTE 1
// This works with no errors!
.get("/file", () => Bun.file("example/path"), {
	response: t.File(),
})

// ROUTE 2
// This also works with no errors!
.get("/nullable-or-optional-file", () => {
	return Bun.file("example/path")
}, {
	response: t.Optional(t.Nullable(t.File())),
})

// ROUTE 3 & 4
// These throw errors, I assume it may having something to do with the
// possibility of needing conversion to multipart form data at runtime or not...
.get("/optional-file-with-more", () => {
	return {
		file: Bun.file("example/path") || undefined,
		text: "hello",
	}
}, {
	response: t.Object({
		file: t.Optional(t.File()),
		text: t.String(),
	}),
})
.get("/nullable-file-with-more", () => {
	return {
		file: Bun.file("example/path") ?? null,
		text: "hello",
	}
}, {
	response: t.Object({
		file: t.Nullable(t.File()),
		text: t.String(),
	}),
})

// ROUTE 5
// Multiple files also just don't work, even without the optional/nullable stuff.
.get("/multiples-files", () => {
	const files = [Bun.file("example/path"), Bun.file("example/path")]
	return {
		files: files
	}
}, {
	response: t.Object({
		files: t.Files()
	})
})

What is the expected behavior?

No errors and files are successfully returned.

What do you see instead?

Following my reproduction example, here is the TS error output for each route.

ROUTE 3

Argument of type '(ctx: { body: unknown; query: Record<string, string | undefined>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<string | undefined>>; ... 8 more ...; error: <const Code extends "OK" | 200, const T extends Code extends 200 ? { ...; }[Code] : Code extends "Continue" | ... 58 mo...' is not assignable to parameter of type 'InlineHandler<MergeSchema<UnwrapRoute<{ readonly response: { readonly 200: TObject<{ file: TOptional<TUnsafe<File>>; text: TString; }>; }; }, TModule<{}, {}>, "/optional-file-with-more">, MergeSchema<...>, "">, { ...; } & { ...; }, "/optional-file-with-more">'.
  Type '(ctx: { body: unknown; query: Record<string, string | undefined>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<string | undefined>>; ... 8 more ...; error: <const Code extends "OK" | 200, const T extends Code extends 200 ? { ...; }[Code] : Code extends "Continue" | ... 58 mo...' is not assignable to type '(context: { body: unknown; query: Record<string, string | undefined>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<string | undefined>>; ... 8 more ...; error: <const Code extends "OK" | 200, const T extends Code extends 200 ? { ...; }[Code] : Code extends "Continue" | ... 5...'.
    Type '{ file: BunFile; text: string; }' is not assignable to type 'Response | MaybePromise<{ readonly 200: { file?: File | undefined; text: string; }; } | { file?: File | undefined; text: string; } | ElysiaCustomStatusResponse<200, { file?: File | undefined; text: string; }, 200>>'.
      Type '{ file: BunFile; text: string; }' is not assignable to type '{ file?: File | undefined; text: string; }'.
        The types of 'file.name' are incompatible between these types.
          Type 'string | undefined' is not assignable to type 'string'.
            Type 'undefined' is not assignable to type 'string'.

ROUTE 4

Argument of type '(ctx: { body: unknown; query: Record<string, string | undefined>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<string | undefined>>; ... 8 more ...; error: <const Code extends "OK" | 200, const T extends Code extends 200 ? { ...; }[Code] : Code extends "Continue" | ... 58 mo...' is not assignable to parameter of type 'InlineHandler<MergeSchema<UnwrapRoute<{ readonly response: { readonly 200: TObject<{ file: TUnion<[TUnsafe<File>, TNull]>; text: TString; }>; }; }, TModule<{}, {}>, "/nullable-file-with-more">, MergeSchema<...>, "">, { ...; } & { ...; }, "/nullable-file-with-more">'.
  Type '(ctx: { body: unknown; query: Record<string, string | undefined>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<string | undefined>>; ... 8 more ...; error: <const Code extends "OK" | 200, const T extends Code extends 200 ? { ...; }[Code] : Code extends "Continue" | ... 58 mo...' is not assignable to type '(context: { body: unknown; query: Record<string, string | undefined>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<string | undefined>>; ... 8 more ...; error: <const Code extends "OK" | 200, const T extends Code extends 200 ? { ...; }[Code] : Code extends "Continue" | ... 5...'.
    Type '{ file: BunFile; text: string; }' is not assignable to type 'Response | MaybePromise<{ readonly 200: { text: string; file: File | null; }; } | { text: string; file: File | null; } | ElysiaCustomStatusResponse<200, { text: string; file: File | null; }, 200>>'.
      Type '{ file: BunFile; text: string; }' is not assignable to type '{ text: string; file: File | null; }'.
        The types of 'file.name' are incompatible between these types.
          Type 'string | undefined' is not assignable to type 'string'.
            Type 'undefined' is not assignable to type 'string'

ROUTE 5

Argument of type '() => { files: BunFile[]; }' is not assignable to parameter of type 'InlineHandler<MergeSchema<UnwrapRoute<{ readonly response: TObject<{ files: TTransform<TUnsafe<File[]>, File[]>; }>; }, TModule<{}, {}>, "/multiples-files">, MergeSchema<...>, "">, { ...; } & { ...; }, "/multiples-files">'.
  Type '() => { files: BunFile[]; }' is not assignable to type '(context: { body: unknown; query: Record<string, string | undefined>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<string | undefined>>; ... 8 more ...; error: <const Code extends "OK" | 200, const T extends Code extends 200 ? { ...; }[Code] : Code extends "Continue" | ... 5...'.
    Type '{ files: BunFile[]; }' is not assignable to type 'Response | MaybePromise<{ 200: { files: File[]; }; } | { files: File[]; } | ElysiaCustomStatusResponse<200, { files: File[]; }, 200>>'.
      Type '{ files: BunFile[]; }' is not assignable to type '{ files: File[]; }'.
        Types of property 'files' are incompatible.
          Type 'BunFile[]' is not assignable to type 'File[]'.
            Type 'BunFile' is not assignable to type 'File'.
              Types of property 'name' are incompatible.
                Type 'string | undefined' is not assignable to type 'string'.
                  Type 'undefined' is not assignable to type 'string'.

Additional information

Hi again, I've been loving the Elysia experience, especially using the Eden connector with my frontend. Everything has been great except lately I have been finding increasingly more issues with the t.File() and t.Files() TypeBox validators.

In this case, all the errors above seem to boil down to the t.File() implemented by the Elysia accepting only string for the name property while BunFile has string | undefined, so hopefully this is an easy fix for you. I find it weird that for t.File() errors only show when it is not returned alone, but that is probably something implementation specific that I'm not aware.

I also tried using the file() wrapper provided by Elysia instead of Bun.file() as per this documentation here, but this gives yet another different error, for which I have opened #1019 separately as I wasn't sure it was related to this.

I'm surprised to see these very common usage problems not having open issues so I'm worried it be just me that's doing something wrong, please let me know if that is the case. I have tried updating both elysia and @types/bun to the latest version and have cleaned node_modules and bun.lockb many times with each reinstall.

Once again, thank you for all your work on Elysia, I respect the massive undertaking this project is and am really enjoying using it.
I would love to contribute some documentation with more examples for working with files once this is fixed, assuming I'm doing it right! 😁

Have you tried removing the node_modules and bun.lockb and try again yet?

👍

@dy0gu dy0gu added the bug Something isn't working label Jan 20, 2025
@hisamafahri
Copy link
Contributor

hisamafahri commented Jan 26, 2025

I think Typescript gives the error correctly. You can't return a file with a JSON. That's why Typescript complain Type 'ElysiaFile' is not assignable to type 'Response | File | BunFile'.

Take this 2 routes for example:

const app = new Elysia()
  .get(
    "/file-only",
    () => {
      return Bun.file("test.txt");
    },
    {
      response: t.Optional(t.Nullable(t.File())),
    }
  )
  .get(
    "/file-json",
    (ctx) => {
      return {
        file: Bun.file("test.txt"),
        text: "hello",
      };
    },
    {
      response: t.Object({
        file: t.Optional(t.File()),
        text: t.String(),
      }),
    }
  )

The Content-Type headers for /file-only will be application/octet-stream and on the other hand /file-json will be (as you guess it) application/json!

And the /file-json will return a regular json:

{
    "file": {},
    "text": "hello"
}

Edit: I believe this also answers your other similar issue (#1019)

@dy0gu
Copy link
Author

dy0gu commented Jan 26, 2025

Yes I see your point, I also considered that possibility before opening this issue.

The official documentation lists my examples as the correct way to do things, it says the response will automatically be FormData which is great because that's what you expect when mixing text and files. But when implemented, it just doesn't work for anything except the case where a simple t.File() is returned. And in the case of multiple files it's just plain impossible to return them, I've tried even with the simplest of bodies.

https://elysiajs.com/integrations/cheat-sheet#return-a-file

Image

By reading the TypeScript errors I'm pretty this is just a type mismatch where the Elysia t.File() needs the possibility of name being undefined (for some reason) to match Bun.file().

So I don't think this solves this issue no... please correct me if I'm missing anything.

@hisamafahri
Copy link
Contributor

Then maybe, this what you intended for: https://elysiajs.com/essential/handler#formdata

@dy0gu
Copy link
Author

dy0gu commented Jan 27, 2025

Tried both the original from the cheat sheet docs and the one you just gave me with the from({}) handler, both break when adding a schema too, using either Bun.file() or the Elysia.file() wrapper:

// ROUTE 1
.get("/like-cheat-sheet-documentation-but-with-formdata-typebox", () => form({
	hello: 'Elysia',
	image: file("example.png")
}),
{
	response: t.Object({
		hello: t.String(),
		image: t.File()
	})
})

// ROUTE 2
.get("/like-handler-formdata-documentation-but-with-typebox", () => form({
	name: 'Tea Party',
	images: [file("example.png"), file("example.png")]
}),
{
	response: t.Object({
		name: t.String(),
		images: t.Files(),
	})
})

ROUTE 1:

Argument of type '() => ElysiaFormData<{ readonly hello: "Elysia"; readonly image: BunFile; }>' is not assignable to parameter of type 'InlineHandler<MergeSchema<UnwrapRoute<{ readonly response: TObject<{ hello: TString; image: TUnsafe<File>; }>; }, TModule<{}, {}>, "/like-cheat-sheet-documentation-but-with-formdata-typebox">, MergeSchema<...>, "">, { ...; } & { ...; }, "/like-cheat-sheet-documentation-but-with-formdata-typebox">'.
  Type '() => ElysiaFormData<{ readonly hello: "Elysia"; readonly image: BunFile; }>' is not assignable to type '(context: { body: unknown; query: Record<string, string | undefined>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<string | undefined>>; ... 8 more ...; error: <const Code extends "OK" | 200, const T extends Code extends 200 ? { ...; }[Code] : Code extends "Continue" | ... 5...'.
    Type 'ElysiaFormData<{ readonly hello: "Elysia"; readonly image: BunFile; }>' is not assignable to type 'Response | MaybePromise<{ 200: { hello: string; image: File | BunFile; }; } | { hello: string; image: File | BunFile; } | ElysiaCustomStatusResponse<200, { hello: string; image: File | BunFile; }, 200>>'.
      Type 'FormData & { [ELYSIA_FORM_DATA]: { readonly hello: "Elysia"; readonly image: File; }; }' is missing the following properties from type 'Promise<{ 200: { hello: string; image: File | BunFile; }; } | { hello: string; image: File | BunFile; } | ElysiaCustomStatusResponse<200, { hello: string; image: File | BunFile; }, 200>>': then, catch, finally

ROUTE 2:

Argument of type '() => ElysiaFormData<{ readonly name: "Tea Party"; readonly images: readonly [ElysiaFile, ElysiaFile]; }>' is not assignable to parameter of type 'InlineHandler<MergeSchema<UnwrapRoute<{ readonly response: TObject<{ name: TString; images: TTransform<TUnsafe<File[]>, File[]>; }>; }, TModule<{}, {}>, "/like-handler-formdata-documentation-but-with-typebox">, MergeSchema<...>, "">, { ...; } & { ...; }, "/like-handler-formdata-documentation-but-with-typebox">'.
  Type '() => ElysiaFormData<{ readonly name: "Tea Party"; readonly images: readonly [ElysiaFile, ElysiaFile]; }>' is not assignable to type '(context: { body: unknown; query: Record<string, string | undefined>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<string | undefined>>; ... 8 more ...; error: <const Code extends "OK" | 200, const T extends Code extends 200 ? { ...; }[Code] : Code extends "Continue" | ... 5...'.
    Type 'ElysiaFormData<{ readonly name: "Tea Party"; readonly images: readonly [ElysiaFile, ElysiaFile]; }>' is not assignable to type 'Response | MaybePromise<{ 200: { name: string; images: File[]; }; } | { name: string; images: File[]; } | ElysiaCustomStatusResponse<200, { name: string; images: File[]; }, 200>>'.
      Type 'FormData & { [ELYSIA_FORM_DATA]: { readonly name: "Tea Party"; readonly images: readonly [ElysiaFile, ElysiaFile]; }; }' is missing the following properties from type 'Promise<{ 200: { name: string; images: File[]; }; } | { name: string; images: File[]; } | ElysiaCustomStatusResponse<200, { name: string; images: File[]; }, 200>>': then, catch, finally

I would like to note that all these things I'm mentioning work by themselves, it's just adding the TypeBox schema that breaks everything, which is unfortunate the TypeBox + Eden combo is one of the main reasons for my usage and migration to Elysia.

Anyways, thanks for all your help and suggestions @hisamafahri, hopefully this will get sorted out or we figure out a workaround in the meantime.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants