Skip to main content

Result Pattern

Il Result Pattern è utilizzato in tutta Tess UI Library per gestire operazioni che possono avere successo o fallire in modo type-safe ed esplicito.

Panoramica

Il Result Pattern sostituisce il tradizionale throwing/catching di eccezioni con un approccio più funzionale e type-safe che:

  • Forza la gestione degli errori: Il tipo Result richiede di controllare success/failure
  • Type-safe: Supporto completo TypeScript generics
  • Esplicito: Chiaro quando un'operazione può fallire
  • Chainable: Supporta composizione funzionale
  • Testabile: Facilità di testing

Result Interface

interface Result<T> {
isSuccess: boolean;
isFailure: boolean;
value: T; // Disponibile se isSuccess
errors: ResultError[]; // Disponibile se isFailure
errorMessage: string; // Primo messaggio di errore
}

interface ResultError {
code: string;
message: string;
field?: string;
metadata?: Record<string, any>;
}

Uso Base

Controllo Success/Failure

import { Result } from '@tess-ui-library/core';

function divide(a: number, b: number): Result<number> {
if (b === 0) {
return Result.fail({
code: 'DIVISION_BY_ZERO',
message: 'Impossibile dividere per zero',
});
}

return Result.ok(a / b);
}

// Uso
const result = divide(10, 2);

if (result.isSuccess) {
console.log('Risultato:', result.value); // 5
} else {
console.error('Errore:', result.errorMessage);
result.errors.forEach(err => {
console.error(\`[\${err.code}] \${err.message}\`);
});
}

Pattern Matching

const result = divide(10, 0);

result.match({
ok: (value) => console.log('Successo:', value),
fail: (errors) => console.error('Errore:', errors),
});

Creazione Result

Success

// Valore semplice
const result = Result.ok(42);

// Oggetto complesso
const result = Result.ok({
id: 1,
name: 'John Doe',
email: 'john@example.com',
});

// Void (operazione senza valore di ritorno)
const result = Result.ok<void>(undefined);

Failure

// Errore singolo
const result = Result.fail({
code: 'NOT_FOUND',
message: 'Utente non trovato',
});

// Errori multipli
const result = Result.fail([
{
code: 'REQUIRED',
message: 'Il campo email è obbligatorio',
field: 'email',
},
{
code: 'INVALID_FORMAT',
message: 'Il formato della password non è valido',
field: 'password',
},
]);

// Con metadata
const result = Result.fail({
code: 'VALIDATION_ERROR',
message: 'Validazione fallita',
metadata: {
timestamp: new Date(),
userId: 123,
},
});

Esempi Pratici

Service Layer

@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);

getUser(id: string): Observable<Result<User>> {
return this.http.get<User>(\`/api/users/\${id}\`).pipe(
map(user => Result.ok(user)),
catchError(error => {
if (error.status === 404) {
return of(Result.fail({
code: 'USER_NOT_FOUND',
message: \`Utente con ID \${id} non trovato\`,
}));
}

return of(Result.fail({
code: 'SERVER_ERROR',
message: 'Errore del server',
metadata: { originalError: error },
}));
})
);
}

createUser(userData: CreateUserDto): Observable<Result<User>> {
return this.http.post<User>('/api/users', userData).pipe(
map(user => Result.ok(user)),
catchError(error => {
if (error.status === 400) {
// Errori di validazione dal backend
const errors = error.error.errors.map((err: any) => ({
code: err.code,
message: err.message,
field: err.field,
}));
return of(Result.fail(errors));
}

return of(Result.fail({
code: 'CREATE_USER_ERROR',
message: 'Impossibile creare l\'utente',
}));
})
);
}
}

Component Usage

@Component({
selector: 'app-user-detail',
standalone: true,
template: `
@if (loading) {
<p-progressSpinner></p-progressSpinner>
} @else if (user) {
<div class="user-card">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
} @else if (errorMessage) {
<p-message severity="error" [text]="errorMessage"></p-message>
}
`,
})
export class UserDetailComponent implements OnInit {
private userService = inject(UserService);
private route = inject(ActivatedRoute);

user: User | null = null;
errorMessage: string = '';
loading = false;

ngOnInit() {
const userId = this.route.snapshot.params['id'];
this.loadUser(userId);
}

loadUser(userId: string) {
this.loading = true;

this.userService.getUser(userId).subscribe(result => {
this.loading = false;

if (result.isSuccess) {
this.user = result.value;
} else {
this.errorMessage = result.errorMessage;

// Log dettagliato degli errori
result.errors.forEach(err => {
console.error(\`[\${err.code}] \${err.message}\`, err.metadata);
});
}
});
}
}

Form Submission

@Component({
selector: 'app-user-form',
standalone: true,
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="field">
<label>Nome</label>
<input formControlName="name" />
@if (getFieldError('name')) {
<small class="error">{{ getFieldError('name') }}</small>
}
</div>

<div class="field">
<label>Email</label>
<input formControlName="email" />
@if (getFieldError('email')) {
<small class="error">{{ getFieldError('email') }}</small>
}
</div>

@if (generalError) {
<p-message severity="error" [text]="generalError"></p-message>
}

<button type="submit" [disabled]="form.invalid || loading">
Salva
</button>
</form>
`,
})
export class UserFormComponent {
private userService = inject(UserService);
private toastService = inject(TessToastService);

form = new FormGroup({
name: new FormControl('', Validators.required),
email: new FormControl('', [Validators.required, Validators.email]),
});

fieldErrors: Record<string, string> = {};
generalError: string = '';
loading = false;

onSubmit() {
if (this.form.invalid) return;

this.loading = true;
this.fieldErrors = {};
this.generalError = '';

this.userService.createUser(this.form.value).subscribe(result => {
this.loading = false;

if (result.isSuccess) {
this.toastService.success('Utente creato con successo!');
this.form.reset();
} else {
// Separa errori per campo da errori generali
const fieldErrors = result.errors.filter(err => err.field);
const generalErrors = result.errors.filter(err => !err.field);

fieldErrors.forEach(err => {
if (err.field) {
this.fieldErrors[err.field] = err.message;
}
});

if (generalErrors.length > 0) {
this.generalError = generalErrors.map(e => e.message).join(', ');
}
}
});
}

getFieldError(field: string): string | null {
return this.fieldErrors[field] || null;
}
}

Storage Operations

@Injectable({ providedIn: 'root' })
export class UserPreferencesService {
private localStorage = inject(TessLocalStorageService);
private logger = inject(LoggerService);

savePreferences(prefs: UserPreferences): Result<void> {
const result = this.localStorage.set('user-preferences', prefs);

if (result.isSuccess) {
this.logger.info('Preferenze salvate con successo');
} else {
this.logger.error('Errore salvataggio preferenze', {
errors: result.errors,
});
}

return result;
}

loadPreferences(): Result<UserPreferences> {
const result = this.localStorage.get<UserPreferences>('user-preferences');

if (result.isFailure) {
this.logger.warn('Impossibile caricare preferenze', {
errors: result.errors,
});

// Ritorna preferenze di default
return Result.ok(this.getDefaultPreferences());
}

if (!result.value) {
return Result.ok(this.getDefaultPreferences());
}

return result;
}

private getDefaultPreferences(): UserPreferences {
return {
theme: 'light',
language: 'it',
notifications: true,
};
}
}

Chaining e Composizione

// Combina più operazioni
function processUser(userId: string): Result<ProcessedUser> {
const userResult = getUserById(userId);
if (userResult.isFailure) {
return Result.fail(userResult.errors);
}

const validationResult = validateUser(userResult.value);
if (validationResult.isFailure) {
return Result.fail(validationResult.errors);
}

const processed = transformUser(userResult.value);
return Result.ok(processed);
}

// Con helper map
function processUserFunctional(userId: string): Result<ProcessedUser> {
return getUserById(userId)
.map(validateUser)
.map(transformUser);
}

Best Practices

  1. Sempre controlla isSuccess/isFailure: Non accedere mai a value senza controllo
  2. Usa codici errore: Definisci costanti per i codici errore
  3. Messaggi descrittivi: Fornisci messaggi di errore chiari per l'utente
  4. Log metadata: Includi informazioni utili per il debug
  5. Field-specific errors: Usa il campo field per errori di validazione form
  6. Combina con Observable: Il pattern si combina bene con RxJS
  7. Testing: Result rende il testing più semplice e deterministico

Codici Errore Standard

// error-codes.ts
export const ErrorCodes = {
// Generic
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
NETWORK_ERROR: 'NETWORK_ERROR',
SERVER_ERROR: 'SERVER_ERROR',

// Validation
REQUIRED: 'REQUIRED',
INVALID_FORMAT: 'INVALID_FORMAT',
MIN_LENGTH: 'MIN_LENGTH',
MAX_LENGTH: 'MAX_LENGTH',

// Business logic
NOT_FOUND: 'NOT_FOUND',
ALREADY_EXISTS: 'ALREADY_EXISTS',
PERMISSION_DENIED: 'PERMISSION_DENIED',
OPERATION_FAILED: 'OPERATION_FAILED',
} as const;

export type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes];

Vedi Anche