Last active
April 14, 2024 12:25
-
-
Save ultrox/c0f04838f2dba3467e1fa72c1872c45b to your computer and use it in GitHub Desktop.
Here is solid concept for client using Zod and Ts.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
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