Mapped Types Not Inferring From Promise Correctly

TS mapped types not behaving as expected on promise inference Desired DX
export const FileRouter = UploadThingServerHelper({
someKey: {
middleware: async (req) => {
const user = await auth(req);

if (!user.userId) throw new Error("someProperty is required");

return { userIds: user.userId };
},
onUpload: async (request) => {
console.log("uploaded with the following metadata:", request.metadata); // THIS SHOULD INFER THE TYPE FROM THE RETURN
},
},
});
export const FileRouter = UploadThingServerHelper({
someKey: {
middleware: async (req) => {
const user = await auth(req);

if (!user.userId) throw new Error("someProperty is required");

return { userIds: user.userId };
},
onUpload: async (request) => {
console.log("uploaded with the following metadata:", request.metadata); // THIS SHOULD INFER THE TYPE FROM THE RETURN
},
},
});
Package types
type UploadableInputParams<TMiddlewareResponse> = {
middleware: (request: Request) => Promise<TMiddlewareResponse>;
onUpload: (response: {
metadata: TMiddlewareResponse;
}) => void;
};

export const UploadThingServerHelper = <TValidRoutes>(
input: {
readonly [K in keyof TValidRoutes]: UploadableInputParams<TValidRoutes[K]>;
},
config?: { callbackUrl?: string }
) => {...businessLogic};
type UploadableInputParams<TMiddlewareResponse> = {
middleware: (request: Request) => Promise<TMiddlewareResponse>;
onUpload: (response: {
metadata: TMiddlewareResponse;
}) => void;
};

export const UploadThingServerHelper = <TValidRoutes>(
input: {
readonly [K in keyof TValidRoutes]: UploadableInputParams<TValidRoutes[K]>;
},
config?: { callbackUrl?: string }
) => {...businessLogic};
Currently have "unknown" coming through in the onUpload type. If I manually define the type on request (i.e. onUpload: async (request: {metadata: {userId: string}})), it will "backfill" correctly Failing to get the type to infer off Promise<T> to the onUpload input
111 Replies
theo (t3.gg)
theo (t3.gg)OP2y ago
Also this would attach the return type as a promise I think Yeah expected the awaited Yeah something's weird here Seemed like such a simple case
Rhys
Rhys2y ago
Is this solving the problem in the right location? Removing the generic input definition for a moment and replacing it with:
export const UploadThingServerHelper = <TValidRoutes>(
input: {
readonly [K in keyof TValidRoutes]: UploadableInputParams<TValidRoutes[K]>;
},
config?: { callbackUrl?: string },
) => {
};
export const UploadThingServerHelper = <TValidRoutes>(
input: {
readonly [K in keyof TValidRoutes]: UploadableInputParams<TValidRoutes[K]>;
},
config?: { callbackUrl?: string },
) => {
};
generates the correct type for metadata it makes me thing that it needs to be solved at this line here, this doesn't work but is along the lines of what im trying
input: {
readonly [K in keyof TValidRoutes]: UploadableInputParams<TValidRoutes[K]['middleware']>;
},
input: {
readonly [K in keyof TValidRoutes]: UploadableInputParams<TValidRoutes[K]['middleware']>;
},
theo (t3.gg)
theo (t3.gg)OP2y ago
This is roughly how I planned on solving it before realizing I'm not good enough at TS and posting here Is @trash_dev in this server? Cool he is hi trash pls help
NickServ
NickServ2y ago
I think you want a template type?
theo (t3.gg)
theo (t3.gg)OP2y ago
I'm not paid enough to know what this means Went down a rabbit hole with infer for a bit and it was rough
NickServ
NickServ2y ago
Assuming the key name is different when you map it (it seemed like that in your Tweet)
theo (t3.gg)
theo (t3.gg)OP2y ago
input here has many keys It's the first arg, second one is config (which i hate and will change eventually)
NickServ
NickServ2y ago
Oh I misread it 🤦‍♂️ Can you give an example of how this helper is consumed (assuming inference is implemented correctly)?
theo (t3.gg)
theo (t3.gg)OP2y ago
Irrelevant to the prompt, the inference within the helper's arguments are bad
export const FileRouter = UploadThingServerHelper({
someKey: {
middleware: async (req) => {
const user = await auth(req);

if (!user.userId) throw new Error("someProperty is required");

return { userIds: user.userId };
},
onUpload: async (request) => {
console.log("uploaded with the following metadata:", request.metadata); // INFERENCE IS BROKEN HERE THAT IS ALL I CARE ABOUT
},
},
});
export const FileRouter = UploadThingServerHelper({
someKey: {
middleware: async (req) => {
const user = await auth(req);

if (!user.userId) throw new Error("someProperty is required");

return { userIds: user.userId };
},
onUpload: async (request) => {
console.log("uploaded with the following metadata:", request.metadata); // INFERENCE IS BROKEN HERE THAT IS ALL I CARE ABOUT
},
},
});
NickServ
NickServ2y ago
Yes I'm aware it's not working, I'm asking how a dependent type would look assuming it was working Is it like tRPC and I just call something like useRouter(router).someKey?
theo (t3.gg)
theo (t3.gg)OP2y ago
Like how FileRouter's type looks? It exports a POST wrapper lol
// some/route/route.ts
import FileRouter from "./filerouter"
export const { POST } = FileRouter;
// some/route/route.ts
import FileRouter from "./filerouter"
export const { POST } = FileRouter;
nexxel
nexxel2y ago
so request.metadata should have the return type of middleware?
theo (t3.gg)
theo (t3.gg)OP2y ago
Yes
cornflour
cornflour2y ago
export const UploadThingServerHelper = <TValidRoutes, TMiddlewareResponse>(
input: {
readonly [K in keyof TValidRoutes]: {
middleware: (request: Request) => Promise<TMiddlewareResponse>
onUpload: (response: { metadata: TMiddlewareResponse }) => void
}
},
config?: { callbackUrl?: string }
) => {...businessLogic}
export const UploadThingServerHelper = <TValidRoutes, TMiddlewareResponse>(
input: {
readonly [K in keyof TValidRoutes]: {
middleware: (request: Request) => Promise<TMiddlewareResponse>
onUpload: (response: { metadata: TMiddlewareResponse }) => void
}
},
config?: { callbackUrl?: string }
) => {...businessLogic}
does this work? oh no it wont if different keys return different types of response
Rhys
Rhys2y ago
im taking my gold star away <:sad_cowboy:632072121945817108>
cornflour
cornflour2y ago
lmao
theo (t3.gg)
theo (t3.gg)OP2y ago
This was how I ended up in the keyof hell If I presume all the middleware is the same this is way easier
cornflour
cornflour2y ago
i'm glad im not a ts library writer lol
theo (t3.gg)
theo (t3.gg)OP2y ago
Right I am suffering so most people don’t have to I’m so mad I got zact right first try and not this one lol
NickServ
NickServ2y ago
I used to work on Redux Toolkit and it's in a different dimension of understanding TS than what I have now
theo (t3.gg)
theo (t3.gg)OP2y ago
Might change the syntax to be more tRPC procedure like but that’s 10x harder to implement and I’ll have to do a bunch of weird this bindings
theo (t3.gg)
theo (t3.gg)OP2y ago
FUCKING CHRIST CHATGPT* GOT IT FIRST TRY
No description
Rhys
Rhys2y ago
we're all fucked
theo (t3.gg)
theo (t3.gg)OP2y ago
No description
theo (t3.gg)
theo (t3.gg)OP2y ago
Not copilot Chatgpt Regardless We're so fucked
NickServ
NickServ2y ago
No idea how this works
nexxel
nexxel2y ago
it makes sense tho
cornflour
cornflour2y ago
crazy
NickServ
NickServ2y ago
I mean, why does onUpload need its own generic parameter?
Rhys
Rhys2y ago
im still trynna read through and understand it it feels wrong to me UploadableInputParams<TValidRoutes[K]>; That is being given:
middleware: async (req) => {
return { some: 'metadata' };
},
onUpload: async (request) => {
request.
},
middleware: async (req) => {
return { some: 'metadata' };
},
onUpload: async (request) => {
request.
},
So TMiddlewareResponse would be that dict object, which I have no idea how that gets to become the return type value of middleware since nothing is converting it to that
cornflour
cornflour2y ago
am i dumb or will it show error if you try to access its props?
No description
Rhys
Rhys2y ago
im guessing there were some modifications to UploadThingServerHelper as well
NickServ
NickServ2y ago
No TS bot? 😢 The more I troubleshoot this the less I understand
devagr
devagr2y ago
so it's done? chatgpt got the right solution?
NickServ
NickServ2y ago
My guess is that we need some conditional type fuckery to get the compiler to apply the homomorphism properly No, keying off the metadata breaks inference https://discord.com/channels/966627436387266600/1097033800019615827/1097053896108679219
theo (t3.gg)
theo (t3.gg)OP2y ago
I'll make a playground off it in a bit, hanging with friends rn
julius
julius2y ago
what's broken in this one?
theo (t3.gg)
theo (t3.gg)OP2y ago
I asked chatgpt more and it gave a better answer
No description
theo (t3.gg)
theo (t3.gg)OP2y ago
Specifically this:
function createUploadableInputParams<TMiddlewareResponse>(
middleware: (request: Request) => Promise<TMiddlewareResponse>,
onUpload: (response: { metadata: TMiddlewareResponse }) => void
): UploadableInputParams<TMiddlewareResponse> {
return { middleware, onUpload };
}
function createUploadableInputParams<TMiddlewareResponse>(
middleware: (request: Request) => Promise<TMiddlewareResponse>,
onUpload: (response: { metadata: TMiddlewareResponse }) => void
): UploadableInputParams<TMiddlewareResponse> {
return { middleware, onUpload };
}
Annoying but I can add a branded type and live with it
julius
julius2y ago
why did you leave the builder pattern?
export const FileRouter = UploadThingServerHelper({
someKey: f
.middleware(async (req) => {
const user = await auth(req);

if (!user.userId) throw new Error("someProperty is required");

return { userIds: user.userId };
})
.onUpload(async (request) => {
console.log("uploaded with the following metadata:", request.metadata); // THIS SHOULD INFER THE TYPE FROM THE RETURN
})
},
});
export const FileRouter = UploadThingServerHelper({
someKey: f
.middleware(async (req) => {
const user = await auth(req);

if (!user.userId) throw new Error("someProperty is required");

return { userIds: user.userId };
})
.onUpload(async (request) => {
console.log("uploaded with the following metadata:", request.metadata); // THIS SHOULD INFER THE TYPE FROM THE RETURN
})
},
});
julius
julius2y ago
oh i see now xdd
cornflour
cornflour2y ago
i like this pattern
NickServ
NickServ2y ago
Are middleware and onUpload always required?
theo (t3.gg)
theo (t3.gg)OP2y ago
This was too hard to do, can I pay you to do it for me lol
julius
julius2y ago
you have a reference codebase you can snag from 🙂
theo (t3.gg)
theo (t3.gg)OP2y ago
That reference codebase is like 2 standard deviations away from my comprehension You’re in our GitHub org already I’ll invite you Early warning I’m also drunk with friends and on mobile rn lmao
NickServ
NickServ2y ago
You should get that balmer peak git plugin
theo (t3.gg)
theo (t3.gg)OP2y ago
There's a git plugin? 👀
NickServ
NickServ2y ago
GitHub
GitHub - noidontdig/gitdown: Don't commit when you're drunk
Don't commit when you're drunk. Contribute to noidontdig/gitdown development by creating an account on GitHub.
NickServ
NickServ2y ago
A college friend of mine had it in Ballmer mode so he could only commit while drunk enough
devagr
devagr2y ago
same question
julius
julius2y ago
on it
No description
julius
julius2y ago
im close
No description
julius
julius2y ago
going out - will come back later
julius
julius2y ago
got it @theo
No description
trash
trash2y ago
i just woke up, this is the longest thread ever did you reference the trpc middleware stuff @marminge
julius
julius2y ago
Oh yes 🙄
trash
trash2y ago
👀👀
julius
julius2y ago
No piping though so a lot simpler
trash
trash2y ago
guessing you couldn’t chain without builder pattern yeah?
julius
julius2y ago
AFAIK you can’t infer return type of a different object key as input to another - that’s where the builder comes in Builder FTW
trash
trash2y ago
sounds about right i bet you can but if it works it works deranged
julius
julius2y ago
builder is the only GoF pattern you ever need 😉
trash
trash2y ago
stare you share the working code @marminge 🙂
julius
julius2y ago
When I get home
trash
trash2y ago
sweet, still in bed no rush my non builder pattern way
// Package

type UploadedFile = unknown;
type AnyUploadableInputParams<T = any> = {
middleware(request: Request): T
onUpload(response: {
metadata: T;
file: UploadedFile;
}): void;
};

function identityFn<TMiddlewareResponse>(
middleware: (request: Request) => Promise<TMiddlewareResponse>,
onUpload: (response: { metadata: TMiddlewareResponse }) => void
): AnyUploadableInputParams {
return {
middleware,
onUpload
};
}

export declare function UploadThingServerHelper<TValidRoutes extends Record<string, AnyUploadableInputParams>>(
input: TValidRoutes,
config?: { callbackUrl?: string }
): void;

// Example

declare function auth(request: Request): { userId: number };

export const FileRouter = UploadThingServerHelper({
someKey: identityFn(
async (request) => {
const user = await auth(request);

if (!user.userId) throw new Error("someProperty is required");

return { userIds: user.userId };
},
async (request) => {
console.log("uploaded with the following metadata:", request.metadata);
// ^?
},
),
});
// Package

type UploadedFile = unknown;
type AnyUploadableInputParams<T = any> = {
middleware(request: Request): T
onUpload(response: {
metadata: T;
file: UploadedFile;
}): void;
};

function identityFn<TMiddlewareResponse>(
middleware: (request: Request) => Promise<TMiddlewareResponse>,
onUpload: (response: { metadata: TMiddlewareResponse }) => void
): AnyUploadableInputParams {
return {
middleware,
onUpload
};
}

export declare function UploadThingServerHelper<TValidRoutes extends Record<string, AnyUploadableInputParams>>(
input: TValidRoutes,
config?: { callbackUrl?: string }
): void;

// Example

declare function auth(request: Request): { userId: number };

export const FileRouter = UploadThingServerHelper({
someKey: identityFn(
async (request) => {
const user = await auth(request);

if (!user.userId) throw new Error("someProperty is required");

return { userIds: user.userId };
},
async (request) => {
console.log("uploaded with the following metadata:", request.metadata);
// ^?
},
),
});
i think that works? 🤷‍♂️
julius
julius2y ago
that looked liek it was working but it wasnt
julius
julius2y ago
oh i was gonna send my builder hang on
trash
trash2y ago
click my link
julius
julius2y ago
ah yea it doesn't have the same problem wizard
trash
trash2y ago
you need an identity fn thingy idk real terms
julius
julius2y ago
yeye that's the difference
trash
trash2y ago
isnt it working in that one? in the one that you linked
julius
julius2y ago
this is the gist of the builder:
type AllowedFiles = "image" | "video" | "audio" | "blob";

type SizeUnit = "B" | "KB" | "MB" | "GB";
type FileSize = `${number}${SizeUnit}`;

type MiddlewareFn = (req: Request) => MaybePromise<unknown>;

export interface UploadBuilderDef<TParams extends AnyParams> {
metadata: Record<string, any>;
middleware: MiddlewareFn;
fileTypes: AllowedFiles[];
maxSize: FileSize;
}

export interface Uploader<TParams extends AnyParams> {
_def: TParams & UploadBuilderDef<TParams>;
}

export type FileRouter = Record<string, Uploader<any>>;

export type CreateProcedureResult<
TPrev extends AnyParams,
TNext extends AnyParams
> = UploadBuilder<{
_metadata: Overwrite<TPrev["_metadata"], TNext["_metadata"]>;
}>;

export interface UploadBuilder<TParams extends AnyParams> {
mimeTypes: (mimes: string[]) => UploadBuilder<TParams>;
maxSize: (size: FileSize) => UploadBuilder<TParams>;
middleware: <TOutput extends object>(
fn: (req: Request) => MaybePromise<TOutput>
) => CreateProcedureResult<TParams, { _metadata: TOutput }>;
onUpload: (
fn: (opts: { metadata: TParams["_metadata"]; file: UploadedFile }) => void
) => Uploader<TParams>;
}
type AllowedFiles = "image" | "video" | "audio" | "blob";

type SizeUnit = "B" | "KB" | "MB" | "GB";
type FileSize = `${number}${SizeUnit}`;

type MiddlewareFn = (req: Request) => MaybePromise<unknown>;

export interface UploadBuilderDef<TParams extends AnyParams> {
metadata: Record<string, any>;
middleware: MiddlewareFn;
fileTypes: AllowedFiles[];
maxSize: FileSize;
}

export interface Uploader<TParams extends AnyParams> {
_def: TParams & UploadBuilderDef<TParams>;
}

export type FileRouter = Record<string, Uploader<any>>;

export type CreateProcedureResult<
TPrev extends AnyParams,
TNext extends AnyParams
> = UploadBuilder<{
_metadata: Overwrite<TPrev["_metadata"], TNext["_metadata"]>;
}>;

export interface UploadBuilder<TParams extends AnyParams> {
mimeTypes: (mimes: string[]) => UploadBuilder<TParams>;
maxSize: (size: FileSize) => UploadBuilder<TParams>;
middleware: <TOutput extends object>(
fn: (req: Request) => MaybePromise<TOutput>
) => CreateProcedureResult<TParams, { _metadata: TOutput }>;
onUpload: (
fn: (opts: { metadata: TParams["_metadata"]; file: UploadedFile }) => void
) => Uploader<TParams>;
}
nah it looks like it but when you try traversing the metadata object it doesn't work anymore
trash
trash2y ago
ah i see! thats.. dumb
julius
julius2y ago
yep...
trash
trash2y ago
the inlay hints are lying?
julius
julius2y ago
the identity function is prob the thing that fixes it then yea
trash
trash2y ago
yeah you cant infer properly so you need another layer to get the second inference to metadata or something.. idk i just type stuff in until it works i think tanstack router does the same thing(uses fns to create its configs) need to look though
julius
julius2y ago
@trash_dev do you know why this isn't autocompleting?
trash
trash2y ago
are those sizes hardcoded or something?
julius
julius2y ago
type SizeUnit = "B" | "KB" | "MB" | "GB";
type FileSize = `${number}${SizeUnit}`;
type SizeUnit = "B" | "KB" | "MB" | "GB";
type FileSize = `${number}${SizeUnit}`;
trash
trash2y ago
one sec out atm if i had to guess its cause you can have any number of numbers so it doesnt know when to suggest the suffixes
julius
julius2y ago
makes sense - but it doesn't autocomplete even after i type the G for example to GB
trash
trash2y ago
its bugging me now though. id ask in pococks discord it works if you give it a finit set of options
type FileSize = `${1 | 2}${SizeUnit}`;
type FileSize = `${1 | 2}${SizeUnit}`;
number is too broad it doesnt really know when its the end even if you have a G so if theres a max limit you can make your on number validation
julius
julius2y ago
ahh
julius
julius2y ago
i guess 1 10 or 100 is good then
No description
julius
julius2y ago
can i make it so these are autocompletable, but any number is acceptable?
trash
trash2y ago
yeah so you can make something that accounts for like 0 - {yourLimit}
type OneThroughNine = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
type Limit = `${OneThroughNine}${OneThroughNine}`

const x: Limit = ``
type OneThroughNine = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
type Limit = `${OneThroughNine}${OneThroughNine}`

const x: Limit = ``
trash
trash2y ago
Luke Morales (@lukemorales)
@t3dotgg @alexturpin_ @bedesqui BTW, the example goes from 1 all the way up to 99. But you can use the principles and build the types until the number of chapters you need. The snippet below goes from 1 to 53:
From Luke Morales (@lukemorales)
Twitter
julius
julius2y ago
this'll work
No description
trash
trash2y ago
heck yeah
Rhys
Rhys2y ago
I'm surprised this doesn't fix it - I'd expect adding a : would be enough to define the split between number and SizeUnit letting autocomplete kick in
No description
trash
trash2y ago
yeah i don’t think template literal type is smart enough or rather evaluates lazily shrug
phau_del_sagrado_corazon
Can u share your curated references to dive into builder pattern i need a refresh
NickServ
NickServ2y ago
He already mentioned GoF
trash
trash2y ago
probably worth actually saying gof means gang of four ok this as eating me alive and it turns out it was a lot simpler
declare function auth(request: Request): { userId: number };

type Config<T> = {
middleware(request: Request): Promise<T>
onUpload(response: {
metadata: T;
file: any;
}): void;
};

export declare function Helper<T extends Record<string, unknown>>(
input: { [K in keyof T]: Config<T[K]> },
): void;

export const SomeFunc = Helper({
someKey: {
middleware: async (request: Request) => { // <------ YOU HAVE TO ANNOTATE REQUEST
const user = await auth(request);

return { userIds: user.userId };
},
onUpload: async (response) => {
console.log(response.metadata);
// ^?

},
}
});
declare function auth(request: Request): { userId: number };

type Config<T> = {
middleware(request: Request): Promise<T>
onUpload(response: {
metadata: T;
file: any;
}): void;
};

export declare function Helper<T extends Record<string, unknown>>(
input: { [K in keyof T]: Config<T[K]> },
): void;

export const SomeFunc = Helper({
someKey: {
middleware: async (request: Request) => { // <------ YOU HAVE TO ANNOTATE REQUEST
const user = await auth(request);

return { userIds: user.userId };
},
onUpload: async (response) => {
console.log(response.metadata);
// ^?

},
}
});
https://tsplay.dev/mL8dVm cc @theo if you dont put request: Request inference brakes LOL updated code snippet to arrows pointing if you remove that type annotation it breaks.......
julius
julius2y ago
lol
trash
trash2y ago
i made a simpler version of this and asked in matts discord.. and noticed that small fix... im gonna cry but that seems .. kinda whack
theo (t3.gg)
theo (t3.gg)OP2y ago
What the actual fuck I hate TS
trash
trash2y ago
that.. has to be a bug right?
theo (t3.gg)
theo (t3.gg)OP2y ago
You're the wizard not me
trash
trash2y ago
ive been nerd sniped so hard
tom
tom2y ago
If you need more fun
No description
theo (t3.gg)
theo (t3.gg)OP2y ago
So cursed
trash
trash2y ago
thinkies me reading andarists replies
Want results from more Discord servers?
Add your server