Skip to content

Instantly share code, notes, and snippets.

@ultrox
Last active April 14, 2024 12:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ultrox/c0f04838f2dba3467e1fa72c1872c45b to your computer and use it in GitHub Desktop.
Save ultrox/c0f04838f2dba3467e1fa72c1872c45b to your computer and use it in GitHub Desktop.
Here is solid concept for client using Zod and Ts.
/**
Many times I tryed to use fuctional concepts to make the client, like Result and Either and pattern match.
That being said while I like Elm and functional concepts I ended up coding myself to oblivion.
Everything is grate as long as you don't read the absolutelly brutal TypeScript errors when using such a concepts
in language that's not made for it. TypeScript errors are unreadable anyway, using it with functional concepts is even worse.
Hence I settle for middle ground which is this code here.
*/
const BASE_URL = "<TODO>"
import {z, ZodError} from "zod";
/* This is what bothers me the most, how do you specify which errors you can get running this promise. */
/**
* Fetches user by ID.
* @param id The user ID.
* @returns A Promise resolved with the user data.
* @throws {NetworkError} When there is a network issue.
* @throws {ValidationError} When the response fails validation.
* @throws {GenericError} For all other errors.
*/
function fetchUserById(id: number): Promise<z.infer<typeof UserSchema>> {
// TODO: this could be made typesafe as well.
const endpoint = new URL(`${BASE_URL}/user/${id}`);
return get(endpoint, UserSchema.parse)
}
interface NetworkError {
type: 'network';
statusCode: number;
message: string;
detail: string;
}
async function networkError(response:Response): Promise<NetworkError> {
const detail = await response.text();
return {
type: 'network',
statusCode: response.status,
message: `HTTP error! Status: ${response.status}`,
detail
}
}
interface ValidationError {
type: 'validation';
message: string;
errors: z.ZodIssue[];
}
function validationError(error: z.ZodError): ValidationError {
return {
type: 'validation',
message: 'Data validation error',
errors: error.errors
}
}
type FetchError = NetworkError | ValidationError | GenericError;
interface GenericError {
type: 'generic';
message: string;
detail?: unknown;
}
function genericError(error: unknown): GenericError {
return {
type: 'generic',
message: 'Unexpected parsing error',
detail: error
}
}
type Decoder<T> = (data: unknown) => T;
async function get<T>(endpoint: URL, decode: Decoder<T>, options: RequestInit = {}): Promise<T> {
try {
const response = await fetch(endpoint.pathname, options);
if (!response.ok) {
// I decided to reject the promise instead of throwing.
// I typically avoid throwing, since you
// probably need to re-throw, else you
// end up in local context and error dissapers.
return Promise.reject(networkError(response))
}
const data = await response.json();
try {
return decode(data)
} catch (error) {
if (error instanceof z.ZodError) {
return Promise.reject(validationError(error))
}
if(error instanceof Error) {
return Promise.reject(genericError(error))
}
}
} catch (error) {
console.error('Unexpected error:', error);
return Promise.reject(genericError(error))
}
return Promise.reject(genericError(new Error("Unknown")))
}
function isFetchError(error: unknown): error is FetchError {
return typeof error === "object" && error !== null && "type" in error
}
async function displayUser() {
try {
const user = await fetchUserById(1);
console.log('User fetched successfully:', user);
} catch (error) {
if (isFetchError(error)) {
console.error('Network issue:', error.message);
} else {
console.error('Unhandled issue:', error.message);
}
}
}
// User land
const KidSchema = z.object({
name: z.string(),
age: z.number(),
});
const UserSchema = z.object({
name: z.string(),
age: z.number(),
kids: z.array(KidSchema),
nextTimeToReport: z.string(),
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment