TypeSafe external API ?

I am currently communicating with an external API and after falling in love with TRPC, I would like to make this communication with the external API as TypeSafe as possible as well. I am however kinda failing in the basics of figuring out how to return different types, depending on the inputs of my function call. Example, i can call the same endpoint on my external API with the parameter "withItems", which kinda ends up mimicking the Prisma version of "include" - but unlike the great work Prisma has done - I am unsure how to detect if the programmer (in this case - mostly me), has passed this parameter to my API call function and return a different typesafe-type. I therefore always end up returning a mixed result (one type - or the other). My models are these (simplified):
interface Order{
id: number,
name: string,
date: Date,
}

interface Item{
name: string,
value: number,
}

interface Order_Items{
items: Item[];
}
interface Order{
id: number,
name: string,
date: Date,
}

interface Item{
name: string,
value: number,
}

interface Order_Items{
items: Item[];
}
My first attempts where to make a function call where I did something similar to this:
function getOrder(data:{
id:number,
withItems?: boolean,
}): Order | (Order&Order_Items) {
/* here i would call the external API with data as the POST data, but return two different types, if withItems was in the data or not */
}

const theOrder1 = getOrder({id: 1,})
const theOrder2 = getOrder({id: 1, withItems:true});
function getOrder(data:{
id:number,
withItems?: boolean,
}): Order | (Order&Order_Items) {
/* here i would call the external API with data as the POST data, but return two different types, if withItems was in the data or not */
}

const theOrder1 = getOrder({id: 1,})
const theOrder2 = getOrder({id: 1, withItems:true});
However my initial approach resulted in my having to typecast the result from getOrder - since I never succeded in making a "strong" connection between the parameters of the call, and the result. In comes Zod - which i believe would be my saviour here - but I fail to get Zod working. I simply cannot grasp my head around how I would implement Zod in this kind of structure and get type-safe return. Hope you can help (and I have formulated my question clear enough)
21 Replies
Aland
Aland•2y ago
I'm not that good with TS either but setting up zod is easy. First you make a new file and export the schema and the type
import { z } from "zod";

export const testMutate = z.object({
test: z.string().trim()
});

export type TestMutate = z.infer<typeof testMutate>
import { z } from "zod";

export const testMutate = z.object({
test: z.string().trim()
});

export type TestMutate = z.infer<typeof testMutate>
Then in your API route you can parse the request body like this
import { testMutate } from '@/requests/test-mutate';
import { NextResponse } from 'next/server';

export async function GET(req: Request) {
let body = await req.json()
let data = testMutate.parse(body)

console.log("dataaa", data)

return NextResponse.json(data);
}
import { testMutate } from '@/requests/test-mutate';
import { NextResponse } from 'next/server';

export async function GET(req: Request) {
let body = await req.json()
let data = testMutate.parse(body)

console.log("dataaa", data)

return NextResponse.json(data);
}
You can also use the type TestMutate to make the mutation from the client.
barry
barry•2y ago
GitHub
GitHub - mattpocock/zod-fetch: Simple function for building a type-...
Simple function for building a type-safe fetcher with Zod - GitHub - mattpocock/zod-fetch: Simple function for building a type-safe fetcher with Zod
_tweak^prone
_tweak^proneOP•2y ago
validating with Zod i can do - but how would this work with "type-return depends on the parameter in the function call" ? Thanks for the resource-paste - I can see the appeal, but I am not entirely sure how this relates to my problem. I would like to build a function, where the parameter in the function call, has a direct influence on the type(safe) return of the function. For example, the default fetcher doesnt seem to take into account varying returns from the API - depending on the data pasted into the API itself. I could surely do this with items?:Item[] in the object defintion of Zod, but what wouldn't exactly make it type safe in the level I want - I am looking for that "prisma feel", where my type is 100% what the API returns, without having to test if an object is in the return or not, when i use the data.
barry
barry•2y ago
You can't, it either conforms to your type or fails. You can never be 100%
_tweak^prone
_tweak^proneOP•2y ago
Yes but the example you paste above is response-validation - I need response validation and typesafe return depending on the parameters sent to GET.
Aland
Aland•2y ago
Why not have 2 functions for each call? i think that may be way easier than trying to figure out how to do it with TS, it gets complicated when you want to do stuff like that.
_tweak^prone
_tweak^proneOP•2y ago
I agree. It would be an easier way out - and its my "gotto failover". However, I feel this is a great place to learn more typescript / Zod, without taking the "easy way out" so to speak 🙂
Aland
Aland•2y ago
Well good luck, the last time i tried to go deep on TS i just wasted ~2 weeks, everything was going over my head. And even chatgpt didn't help, either it coldn't figure it out or i may be a bad Proompter. Rust was easier to learn than low level TS.
barry
barry•2y ago
const Item = z.object({
id: z.number(),
...
})
const Items = z.array(Item)
const Order = z.object({})
const OrderWithItems = z.object({
...,
items: Items
})

type OrderWithItems = z.infer<OrderWithItems>
type OrderWithoutItems = z.infer<Order>

function getOrder(params: { id: number, withItems: true }): OrderWithItems;
function getOrder(params: { id: number, withItems: false }): OrderWithoutItems;
function getOrder(params: { id: number }): OrderWithoutItems;
function getOrder(params) {
if (!params) throw new Error("...");

if (!params.withItems) {
const result = ...
return Order.parse(result)
}

const result = ...

return OrderWithItems.parse()
}
const Item = z.object({
id: z.number(),
...
})
const Items = z.array(Item)
const Order = z.object({})
const OrderWithItems = z.object({
...,
items: Items
})

type OrderWithItems = z.infer<OrderWithItems>
type OrderWithoutItems = z.infer<Order>

function getOrder(params: { id: number, withItems: true }): OrderWithItems;
function getOrder(params: { id: number, withItems: false }): OrderWithoutItems;
function getOrder(params: { id: number }): OrderWithoutItems;
function getOrder(params) {
if (!params) throw new Error("...");

if (!params.withItems) {
const result = ...
return Order.parse(result)
}

const result = ...

return OrderWithItems.parse()
}
Why would this not do it though I don't get it
Aland
Aland•2y ago
This looks great, you just need to make Items optional
barry
barry•2y ago
no?
Aland
Aland•2y ago
Well if there isn't items it will throw an error?
barry
barry•2y ago
Why would it
Aland
Aland•2y ago
Because it expects items to be defined
barry
barry•2y ago
No It should only reach that if withItems has been set to true, at which we assume he has called the api assuming items will be there
Aland
Aland•2y ago
You need to do
const Items = z.array(Item).optional()
const Items = z.array(Item).optional()
barry
barry•2y ago
No
Aland
Aland•2y ago
Well idk
_tweak^prone
_tweak^proneOP•2y ago
I am sorry, I am unable to say why this would not do it, as I am not as well versed in TS as you seem to be 🙂 Its a in interested approach with more defined function-names refering to the same function - I was not aware / had forgotten this was possible in TS. I do believe this could or would give the desired result - at least it looks like a good match, if i interpret the code correctly. However I could be a bit worried that the complexity of this kind of solution would grow exponentionally with more optional variables being introduced, like withSeller, withBuyer, withStatistics etc. and as such the suggested solution might just cover the actual example i put forth above. The approach you have highlighted here, i believe I would be able to use right out of the box as is however - for this one specific case - but I would need a more generic approach, for multiple options - would you agree ? If anyone else wants to know the answer to this, I managed to build the below code:
rocawear
rocawear•2y ago
But you are not validating it
Want results from more Discord servers?
Add your server