TypeScript gets called verbose when people fight the type system instead of working with it. These three patterns flipped that script for me.
Discriminated Unions for API Responses
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string }
const response: ApiResponse<User> = await fetchUser()
if (response.success) {
// TypeScript knows response.data is User
response.data.email
} else {
// TypeScript knows response.error exists
console.error(response.error)
}
No more if (response.error) guesswork. The compiler guards every branch.
Generic Composables in Vue
// composables/useFetch.ts
export const useFetch = <T>() => {
const data = ref<T | null>(null)
const error = ref<string | null>(null)
const fetch = async (url: string) => {
const res = await fetch(url)
if (!res.ok) {
error.value = 'Failed'
return
}
data.value = await res.json() as T
}
return { data, error, fetch }
}
// Usage knows the exact return type
const { data } = useFetch<UserProfile>()
20 lines upfront, zero any debugging later.
Zod at the Boundary
const userSchema = z.object({
id: z.number(),
email: z.string().email(),
name: z.string()
})
const response = await fetch('/api/user')
const user = userSchema.parse(await response.json())
// Everything past this point is 100% typed
user.email // string, guaranteed
Result: Last Laravel + Vue project refactor eliminated 80% of runtime type errors. The compiler became my QA team.
Ship faster. Type smarter.