Type Safety Guide
Learn about type safety features and best practices in go-errors
Type Safety Guide
This guide covers type safety features and best practices when using the go-errors library.
Core Types
Result Type
The foundation of type safety in go-errors is the Result
type:
type Result<T, E = Error> = readonly [T, null] | readonly [null, E];
This type ensures that:
- You always get either a value or an error, never both
- The tuple is readonly to prevent accidental modifications
- The error type can be customized
- TypeScript can infer the correct types
Generic Type Parameters
Basic Usage
import { goSync, goFetch } from 'go-errors';
// Default error type (Error)
let [num, err] = goSync(() => 42);
// num is number | null
// err is Error | null
// Custom value type
let [str, err] = goSync<string>(() => 'hello');
// str is string | null
// err is Error | null
// Custom error type
let [val, err] = goSync<number, ValidationError>(() => {
throw new ValidationError('value', 'Invalid number');
});
// val is number | null
// err is ValidationError | null
Type Inference
TypeScript can often infer the correct types:
// Type inference from return value
let [num, err] = goSync(() => 42);
// num is inferred as number | null
// Type inference from thrown error
let [val, err] = goSync(() => {
throw new ValidationError('field', 'message');
});
// err is inferred as ValidationError | null
// Type inference in async operations
let [data, err] = await goFetch('/api/data');
// data is inferred from response
Custom Error Types
Defining Error Types
// Basic custom error
class AppError extends Error {
constructor(message: string) {
super(message);
this.name = 'AppError';
}
}
// Error with additional data
class ValidationError extends Error {
constructor(
public field: string,
message: string,
public code: number = 400
) {
super(message);
this.name = 'ValidationError';
}
}
// Union type for all possible errors
type ApplicationError =
| ValidationError
| NotFoundError
| AuthError;
Using Custom Errors
function validateUser(user: User) {
let [valid, err] = goSync<boolean, ValidationError>(() => {
if (!user.name) {
throw new ValidationError('name', 'Name is required');
}
if (!user.email) {
throw new ValidationError('email', 'Email is required');
}
return true;
});
// TypeScript knows err is ValidationError | null
if (err) {
console.error(`Field ${err.field} is invalid:`, err.message);
return [null, err] as const;
}
return [valid, null] as const;
}
HTTP Types
Response Types
interface ApiResponse<T> {
data: T;
metadata: {
timestamp: string;
version: string;
};
}
interface User {
id: number;
firstName: string;
lastName: string;
fullName: string;
email: string;
}
// Type-safe API call
let [response, err] = await goFetch<ApiResponse<User>>('/api/user/1', {
responseTransformer: (data) => ({
...data,
data: {
...data.data,
fullName: `${data.data.firstName} ${data.data.lastName}`
}
})
});
if (err) return [null, err] as const;
// TypeScript knows response.data is User
console.log(response.data.fullName);
Error Types
interface ApiError {
code: string;
message: string;
details?: unknown;
}
// Type-safe error handling
let [data, err] = await goFetch<User, ApiError>('/api/user/1', {
errorTransformer: (error) => {
if (error instanceof Response) {
return {
code: `HTTP_${error.status}`,
message: error.statusText
};
}
return {
code: 'UNKNOWN_ERROR',
message: error instanceof Error ? error.message : String(error)
};
}
});
if (err) {
// TypeScript knows err has code and message
console.error(`API Error ${err.code}: ${err.message}`);
return;
}
Advanced Type Safety
Discriminated Unions
// Define a discriminated union for different error types
type Result<T> =
| { type: 'success'; data: T }
| { type: 'validation'; errors: ValidationError[] }
| { type: 'notFound'; resource: string }
| { type: 'auth'; message: string };
function processResult<T>(result: Result<T>) {
switch (result.type) {
case 'success':
// TypeScript knows result.data exists
return result.data;
case 'validation':
// TypeScript knows result.errors exists
return handleValidationErrors(result.errors);
case 'notFound':
// TypeScript knows result.resource exists
return handleNotFound(result.resource);
case 'auth':
// TypeScript knows result.message exists
return handleAuthError(result.message);
}
}
Type Guards
// Type guard for custom error
function isValidationError(error: unknown): error is ValidationError {
return error instanceof Error &&
'field' in error &&
typeof (error as any).field === 'string';
}
// Using type guards
let [data, err] = goSync(() => validateData());
if (err) {
if (isValidationError(err)) {
// TypeScript knows err has field property
console.error(`Validation failed for ${err.field}`);
} else {
console.error('Unknown error:', err);
}
return;
}
Type Safety with Transformers
Response Transformers
interface ApiUser {
id: string; // API returns string
created_at: string;
updated_at: string;
first_name: string;
last_name: string;
email_address: string;
}
interface User {
id: number; // We want number
createdAt: Date;
updatedAt: Date;
firstName: string;
lastName: string;
fullName: string;
email: string;
}
let [user, err] = await goFetch<User>('/api/user/1', {
responseTransformer: (data: unknown): User => {
const apiUser = data as ApiUser;
return {
id: parseInt(apiUser.id, 10),
createdAt: new Date(apiUser.created_at),
updatedAt: new Date(apiUser.updated_at),
firstName: apiUser.first_name,
lastName: apiUser.last_name,
fullName: `${apiUser.first_name} ${apiUser.last_name}`,
email: apiUser.email_address,
};
}
});
Error Transformers
interface ApiError {
code: string;
message: string;
details?: unknown;
}
let [data, err] = await goFetch<User, ApiError>('/api/user', {
errorTransformer: (error) => {
if (error instanceof Response) {
return {
code: `HTTP_${error.status}`,
message: error.statusText
};
}
if (error instanceof ValidationError) {
return {
code: 'VALIDATION_ERROR',
message: error.message,
details: { field: error.field }
};
}
return {
code: 'UNKNOWN_ERROR',
message: error instanceof Error ? error.message : String(error)
};
}
});
Best Practices
-
Always Use Type Parameters
// ✅ Good: Explicit types let [user, err] = await goFetch<User, ApiError>('/api/user'); // ❌ Bad: Implicit any let [data, err] = await goFetch('/api/user');
-
Use Type Guards
// ✅ Good: Type guard for error handling if (isValidationError(err)) { console.error(`Field ${err.field} is invalid`); } // ❌ Bad: Type assertion console.error(`Field ${(err as ValidationError).field}`);
-
Return Type Safety
// ✅ Good: Use as const for proper inference return [value, null] as const; return [null, err] as const; // ❌ Bad: Without as const return [value, null];