Ελληνικά

Κατακτήστε τα utility types της TypeScript: ισχυρά εργαλεία για μετασχηματισμούς τύπων, βελτιώνοντας την επαναχρησιμοποίηση κώδικα και την ασφάλεια τύπων στις εφαρμογές σας.

Utility Types της TypeScript: Ενσωματωμένα Εργαλεία Χειρισμού Τύπων

Η TypeScript είναι μια ισχυρή γλώσσα που φέρνει στατική τυποποίηση (static typing) στη JavaScript. Ένα από τα βασικά της χαρακτηριστικά είναι η ικανότητα χειρισμού τύπων, επιτρέποντας στους προγραμματιστές να δημιουργούν πιο στιβαρό και συντηρήσιμο κώδικα. Η TypeScript παρέχει ένα σύνολο ενσωματωμένων utility types που απλοποιούν τους κοινούς μετασχηματισμούς τύπων. Αυτοί οι utility types είναι ανεκτίμητα εργαλεία για την ενίσχυση της ασφάλειας τύπων, τη βελτίωση της επαναχρησιμοποίησης του κώδικα και τον εξορθολογισμό της ροής εργασίας ανάπτυξης. Αυτός ο περιεκτικός οδηγός εξερευνά τους πιο βασικούς utility types της TypeScript, παρέχοντας πρακτικά παραδείγματα και χρήσιμες πληροφορίες για να σας βοηθήσει να τους κατακτήσετε.

Τι είναι τα Utility Types της TypeScript;

Οι utility types είναι προκαθορισμένοι τελεστές τύπων που μετασχηματίζουν υπάρχοντες τύπους σε νέους. Είναι ενσωματωμένοι στη γλώσσα TypeScript και παρέχουν έναν συνοπτικό και δηλωτικό τρόπο για την εκτέλεση κοινών χειρισμών τύπων. Η χρήση των utility types μπορεί να μειώσει σημαντικά τον επαναλαμβανόμενο κώδικα (boilerplate) και να κάνει τους ορισμούς των τύπων σας πιο εκφραστικούς και ευκολότερους στην κατανόηση.

Σκεφτείτε τους ως συναρτήσεις που λειτουργούν σε τύπους αντί για τιμές. Παίρνουν έναν τύπο ως είσοδο και επιστρέφουν έναν τροποποιημένο τύπο ως έξοδο. Αυτό σας επιτρέπει να δημιουργείτε σύνθετες σχέσεις και μετασχηματισμούς τύπων με ελάχιστο κώδικα.

Γιατί να Χρησιμοποιήσετε τα Utility Types;

Υπάρχουν αρκετοί επιτακτικοί λόγοι για να ενσωματώσετε τους utility types στα έργα σας με TypeScript:

Βασικοί Utility Types της TypeScript

Ας εξερευνήσουμε μερικούς από τους πιο συχνά χρησιμοποιούμενους και ωφέλιμους utility types στην TypeScript. Θα καλύψουμε τον σκοπό τους, τη σύνταξή τους και θα παρέχουμε πρακτικά παραδείγματα για να επεξηγήσουμε τη χρήση τους.

1. Partial<T>

Ο utility type Partial<T> καθιστά όλες τις ιδιότητες του τύπου T προαιρετικές. Αυτό είναι χρήσιμο όταν θέλετε να δημιουργήσετε έναν νέο τύπο που έχει ορισμένες ή όλες τις ιδιότητες ενός υπάρχοντος τύπου, αλλά δεν θέλετε να απαιτείται η παρουσία όλων.

Σύνταξη:

type Partial<T> = { [P in keyof T]?: T[P]; };

Παράδειγμα:

interface User {
 id: number;
 name: string;
 email: string;
}

type OptionalUser = Partial<User>; // Όλες οι ιδιότητες είναι τώρα προαιρετικές

const partialUser: OptionalUser = {
 name: "Alice", // Παρέχεται μόνο η ιδιότητα name
};

Περίπτωση Χρήσης: Ενημέρωση ενός αντικειμένου με μόνο ορισμένες ιδιότητες. Για παράδειγμα, φανταστείτε μια φόρμα ενημέρωσης προφίλ χρήστη. Δεν θέλετε να απαιτείται από τους χρήστες να ενημερώνουν κάθε πεδίο ταυτόχρονα.

2. Required<T>

Ο utility type Required<T> καθιστά όλες τις ιδιότητες του τύπου T υποχρεωτικές. Είναι το αντίθετο του Partial<T>. Αυτό είναι χρήσιμο όταν έχετε έναν τύπο με προαιρετικές ιδιότητες και θέλετε να διασφαλίσετε ότι όλες οι ιδιότητες είναι παρούσες.

Σύνταξη:

type Required<T> = { [P in keyof T]-?: T[P]; };

Παράδειγμα:

interface Config {
 apiKey?: string;
 apiUrl?: string;
}

type CompleteConfig = Required<Config>; // Όλες οι ιδιότητες είναι τώρα υποχρεωτικές

const config: CompleteConfig = {
 apiKey: "your-api-key",
 apiUrl: "https://example.com/api",
};

Περίπτωση Χρήσης: Επιβολή της παροχής όλων των ρυθμίσεων διαμόρφωσης πριν από την έναρξη μιας εφαρμογής. Αυτό μπορεί να βοηθήσει στην πρόληψη σφαλμάτων χρόνου εκτέλεσης που προκαλούνται από ελλιπείς ή μη καθορισμένες ρυθμίσεις.

3. Readonly<T>

Ο utility type Readonly<T> καθιστά όλες τις ιδιότητες του τύπου T μόνο για ανάγνωση (readonly). Αυτό σας εμποδίζει να τροποποιήσετε κατά λάθος τις ιδιότητες ενός αντικειμένου μετά τη δημιουργία του. Αυτό προωθεί την αμεταβλητότητα (immutability) και βελτιώνει την προβλεψιμότητα του κώδικά σας.

Σύνταξη:

type Readonly<T> = { readonly [P in keyof T]: T[P]; };

Παράδειγμα:

interface Product {
 id: number;
 name: string;
 price: number;
}

type ImmutableProduct = Readonly<Product>; // Όλες οι ιδιότητες είναι τώρα readonly

const product: ImmutableProduct = {
 id: 123,
 name: "Example Product",
 price: 25.99,
};

// product.price = 29.99; // Σφάλμα: Δεν είναι δυνατή η ανάθεση στην ιδιότητα 'price' επειδή είναι ιδιότητα μόνο για ανάγνωση.

Περίπτωση Χρήσης: Δημιουργία αμετάβλητων δομών δεδομένων, όπως αντικείμενα διαμόρφωσης ή αντικείμενα μεταφοράς δεδομένων (DTOs), που δεν πρέπει να τροποποιούνται μετά τη δημιουργία. Αυτό είναι ιδιαίτερα χρήσιμο σε παραδείγματα συναρτησιακού προγραμματισμού.

4. Pick<T, K extends keyof T>

Ο utility type Pick<T, K extends keyof T> δημιουργεί έναν νέο τύπο επιλέγοντας ένα σύνολο ιδιοτήτων K από τον τύπο T. Αυτό είναι χρήσιμο όταν χρειάζεστε μόνο ένα υποσύνολο των ιδιοτήτων ενός υπάρχοντος τύπου.

Σύνταξη:

type Pick<T, K extends keyof T> = { [P in K]: T[P]; };

Παράδειγμα:

interface Employee {
 id: number;
 name: string;
 department: string;
salary: number;
}

type EmployeeNameAndDepartment = Pick<Employee, "name" | "department">; // Επιλέγονται μόνο το όνομα και το τμήμα

const employeeInfo: EmployeeNameAndDepartment = {
 name: "Bob",
 department: "Engineering",
};

Περίπτωση Χρήσης: Δημιουργία εξειδικευμένων αντικειμένων μεταφοράς δεδομένων (DTOs) που περιέχουν μόνο τα απαραίτητα δεδομένα για μια συγκεκριμένη λειτουργία. Αυτό μπορεί να βελτιώσει την απόδοση και να μειώσει τον όγκο των δεδομένων που μεταδίδονται μέσω του δικτύου. Φανταστείτε να στέλνετε λεπτομέρειες χρήστη στον client, αλλά να εξαιρείτε ευαίσθητες πληροφορίες όπως ο μισθός. Θα μπορούσατε να χρησιμοποιήσετε το Pick για να στείλετε μόνο τα `id` και `name`.

5. Omit<T, K extends keyof any>

Ο utility type Omit<T, K extends keyof any> δημιουργεί έναν νέο τύπο παραλείποντας ένα σύνολο ιδιοτήτων K από τον τύπο T. Αυτό είναι το αντίθετο του Pick<T, K extends keyof T> και είναι χρήσιμο όταν θέλετε να εξαιρέσετε ορισμένες ιδιότητες από έναν υπάρχοντα τύπο.

Σύνταξη:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Παράδειγμα:

interface Event {
 id: number;
 title: string;
description: string;
 date: Date;
 location: string;
}

type EventSummary = Omit<Event, "description" | "location">; // Παραλείπονται η περιγραφή και η τοποθεσία

const eventPreview: EventSummary = {
 id: 1,
 title: "Conference",
 date: new Date(),
};

Περίπτωση Χρήσης: Δημιουργία απλοποιημένων εκδόσεων μοντέλων δεδομένων για συγκεκριμένους σκοπούς, όπως η εμφάνιση μιας περίληψης ενός γεγονότος χωρίς να περιλαμβάνεται η πλήρης περιγραφή και τοποθεσία. Μπορεί επίσης να χρησιμοποιηθεί για την αφαίρεση ευαίσθητων πεδίων πριν από την αποστολή δεδομένων σε έναν client.

6. Exclude<T, U>

Ο utility type Exclude<T, U> δημιουργεί έναν νέο τύπο εξαιρώντας από το T όλους τους τύπους που είναι αναθέσιμοι στο U. Αυτό είναι χρήσιμο όταν θέλετε να αφαιρέσετε ορισμένους τύπους από έναν τύπο ένωσης (union type).

Σύνταξη:

type Exclude<T, U> = T extends U ? never : T;

Παράδειγμα:

type AllowedFileTypes = "image" | "video" | "audio" | "document";
type MediaFileTypes = "image" | "video" | "audio";

type DocumentFileTypes = Exclude<AllowedFileTypes, MediaFileTypes>; // "document"

const fileType: DocumentFileTypes = "document";

Περίπτωση Χρήσης: Φιλτράρισμα ενός τύπου ένωσης για την αφαίρεση συγκεκριμένων τύπων που δεν είναι σχετικοί σε ένα συγκεκριμένο πλαίσιο. Για παράδειγμα, μπορεί να θέλετε να εξαιρέσετε ορισμένους τύπους αρχείων από μια λίστα επιτρεπόμενων τύπων αρχείων.

7. Extract<T, U>

Ο utility type Extract<T, U> δημιουργεί έναν νέο τύπο εξάγοντας από το T όλους τους τύπους που είναι αναθέσιμοι στο U. Αυτό είναι το αντίθετο του Exclude<T, U> και είναι χρήσιμο όταν θέλετε να επιλέξετε συγκεκριμένους τύπους από έναν τύπο ένωσης.

Σύνταξη:

type Extract<T, U> = T extends U ? T : never;

Παράδειγμα:

type InputTypes = string | number | boolean | null | undefined;
type PrimitiveTypes = string | number | boolean;

type NonNullablePrimitives = Extract<InputTypes, PrimitiveTypes>; // string | number | boolean

const value: NonNullablePrimitives = "hello";

Περίπτωση Χρήσης: Επιλογή συγκεκριμένων τύπων από έναν τύπο ένωσης με βάση ορισμένα κριτήρια. Για παράδειγμα, μπορεί να θέλετε να εξαγάγετε όλους τους πρωτογενείς τύπους (primitive types) από έναν τύπο ένωσης που περιλαμβάνει τόσο πρωτογενείς τύπους όσο και τύπους αντικειμένων.

8. NonNullable<T>

Ο utility type NonNullable<T> δημιουργεί έναν νέο τύπο εξαιρώντας τα null και undefined από τον τύπο T. Αυτό είναι χρήσιμο όταν θέλετε να διασφαλίσετε ότι ένας τύπος δεν μπορεί να είναι null ή undefined.

Σύνταξη:

type NonNullable<T> = T extends null | undefined ? never : T;

Παράδειγμα:

type MaybeString = string | null | undefined;

type DefinitelyString = NonNullable<MaybeString>; // string

const message: DefinitelyString = "Hello, world!";

Περίπτωση Χρήσης: Επιβολή ότι μια τιμή δεν είναι null ή undefined πριν από την εκτέλεση μιας λειτουργίας σε αυτήν. Αυτό μπορεί να βοηθήσει στην πρόληψη σφαλμάτων χρόνου εκτέλεσης που προκαλούνται από απροσδόκητες τιμές null ή undefined. Σκεφτείτε ένα σενάριο όπου πρέπει να επεξεργαστείτε τη διεύθυνση ενός χρήστη και είναι κρίσιμο η διεύθυνση να μην είναι null πριν από οποιαδήποτε λειτουργία.

9. ReturnType<T extends (...args: any) => any>

Ο utility type ReturnType<T extends (...args: any) => any> εξάγει τον τύπο επιστροφής ενός τύπου συνάρτησης T. Αυτό είναι χρήσιμο όταν θέλετε να γνωρίζετε τον τύπο της τιμής που επιστρέφει μια συνάρτηση.

Σύνταξη:

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

Παράδειγμα:

function fetchData(url: string): Promise<{ data: any }> {
 return fetch(url).then(response => response.json());
}

type FetchDataReturnType = ReturnType<typeof fetchData>; // Promise<{ data: any }>

async function processData(data: FetchDataReturnType) {
 // ...
}

Περίπτωση Χρήσης: Προσδιορισμός του τύπου της τιμής που επιστρέφεται από μια συνάρτηση, ειδικά όταν πρόκειται για ασύγχρονες λειτουργίες ή πολύπλοκες υπογραφές συναρτήσεων. Αυτό σας επιτρέπει να διασφαλίσετε ότι χειρίζεστε σωστά την επιστρεφόμενη τιμή.

10. Parameters<T extends (...args: any) => any>

Ο utility type Parameters<T extends (...args: any) => any> εξάγει τους τύπους παραμέτρων ενός τύπου συνάρτησης T ως μια πλειάδα (tuple). Αυτό είναι χρήσιμο όταν θέλετε να γνωρίζετε τους τύπους των ορισμάτων που δέχεται μια συνάρτηση.

Σύνταξη:

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

Παράδειγμα:

function createUser(name: string, age: number, email: string): void {
 // ...
}

type CreateUserParams = Parameters<typeof createUser>; // [string, number, string]

function logUser(...args: CreateUserParams) {
 console.log("Creating user with:", args);
}

Περίπτωση Χρήσης: Προσδιορισμός των τύπων των ορισμάτων που δέχεται μια συνάρτηση, το οποίο μπορεί να είναι χρήσιμο για τη δημιουργία γενικών συναρτήσεων ή decorators που πρέπει να λειτουργούν με συναρτήσεις διαφορετικών υπογραφών. Βοηθά στη διασφάλιση της ασφάλειας τύπων κατά τη δυναμική διέλευση ορισμάτων σε μια συνάρτηση.

11. ConstructorParameters<T extends abstract new (...args: any) => any>

Ο utility type ConstructorParameters<T extends abstract new (...args: any) => any> εξάγει τους τύπους παραμέτρων ενός τύπου συνάρτησης κατασκευαστή (constructor) T ως μια πλειάδα. Αυτό είναι χρήσιμο όταν θέλετε να γνωρίζετε τους τύπους των ορισμάτων που δέχεται ένας κατασκευαστής.

Σύνταξη:

type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;

Παράδειγμα:

class Logger {
 constructor(public prefix: string, public enabled: boolean) {}
 log(message: string) {
 if (this.enabled) {
 console.log(`${this.prefix}: ${message}`);
 }
 }
}

type LoggerConstructorParams = ConstructorParameters<typeof Logger>; // [string, boolean]

function createLogger(...args: LoggerConstructorParams) {
 return new Logger(...args);
}

Περίπτωση Χρήσης: Παρόμοια με το Parameters, αλλά ειδικά για συναρτήσεις κατασκευαστών. Βοηθά κατά τη δημιουργία factories ή συστημάτων έγχυσης εξαρτήσεων (dependency injection) όπου χρειάζεται να δημιουργείτε δυναμικά στιγμιότυπα κλάσεων με διαφορετικές υπογραφές κατασκευαστών.

12. InstanceType<T extends abstract new (...args: any) => any>

Ο utility type InstanceType<T extends abstract new (...args: any) => any> εξάγει τον τύπο του στιγμιότυπου (instance) ενός τύπου συνάρτησης κατασκευαστή T. Αυτό είναι χρήσιμο όταν θέλετε να γνωρίζετε τον τύπο του αντικειμένου που δημιουργεί ένας κατασκευαστής.

Σύνταξη:

type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;

Παράδειγμα:

class Greeter {
 greeting: string;
 constructor(message: string) {
 this.greeting = message;
 }
 greet() {
 return "Hello, " + this.greeting;
 }
}

type GreeterInstance = InstanceType<typeof Greeter>; // Greeter

const myGreeter: GreeterInstance = new Greeter("World");
console.log(myGreeter.greet());

Περίπτωση Χρήσης: Προσδιορισμός του τύπου του αντικειμένου που δημιουργείται από έναν κατασκευαστή, το οποίο είναι χρήσιμο όταν εργάζεστε με κληρονομικότητα ή πολυμορφισμό. Παρέχει έναν ασφαλή ως προς τον τύπο τρόπο αναφοράς στο στιγμιότυπο μιας κλάσης.

13. Record<K extends keyof any, T>

Ο utility type Record<K extends keyof any, T> κατασκευάζει έναν τύπο αντικειμένου του οποίου τα κλειδιά ιδιοτήτων είναι K και οι τιμές των ιδιοτήτων του είναι T. Αυτό είναι χρήσιμο για τη δημιουργία τύπων που μοιάζουν με λεξικό (dictionary-like) όπου γνωρίζετε τα κλειδιά εκ των προτέρων.

Σύνταξη:

type Record<K extends keyof any, T> = { [P in K]: T; };

Παράδειγμα:

type CountryCode = "US" | "CA" | "GB" | "DE";

type CurrencyMap = Record<CountryCode, string>; // { US: string; CA: string; GB: string; DE: string; }

const currencies: CurrencyMap = {
 US: "USD",
 CA: "CAD",
 GB: "GBP",
 DE: "EUR",
};

Περίπτωση Χρήσης: Δημιουργία αντικειμένων που μοιάζουν με λεξικό όπου έχετε ένα σταθερό σύνολο κλειδιών και θέλετε να διασφαλίσετε ότι όλα τα κλειδιά έχουν τιμές ενός συγκεκριμένου τύπου. Αυτό είναι συνηθισμένο όταν εργάζεστε με αρχεία διαμόρφωσης, αντιστοιχίσεις δεδομένων ή πίνακες αναζήτησης (lookup tables).

Προσαρμοσμένοι Utility Types

Ενώ οι ενσωματωμένοι utility types της TypeScript είναι ισχυροί, μπορείτε επίσης να δημιουργήσετε τους δικούς σας προσαρμοσμένους utility types για να καλύψετε συγκεκριμένες ανάγκες στα έργα σας. Αυτό σας επιτρέπει να ενσωματώσετε πολύπλοκους μετασχηματισμούς τύπων και να τους επαναχρησιμοποιήσετε σε όλη τη βάση κώδικά σας.

Παράδειγμα:

// Ένας utility type για τη λήψη των κλειδιών ενός αντικειμένου που έχουν έναν συγκεκριμένο τύπο
type KeysOfType<T, U> = { [K in keyof T]: T[K] extends U ? K : never }[keyof T];

interface Person {
 name: string;
 age: number;
 address: string;
 phoneNumber: number;
}

type StringKeys = KeysOfType<Person, string>; // "name" | "address"

Βέλτιστες Πρακτικές για τη Χρήση των Utility Types

Συμπέρασμα

Οι utility types της TypeScript είναι ισχυρά εργαλεία που μπορούν να βελτιώσουν σημαντικά την ασφάλεια τύπων, την επαναχρησιμοποίηση και τη συντηρησιμότητα του κώδικά σας. Κατακτώντας αυτούς τους utility types, μπορείτε να γράψετε πιο στιβαρές και εκφραστικές εφαρμογές TypeScript. Αυτός ο οδηγός κάλυψε τους πιο βασικούς utility types της TypeScript, παρέχοντας πρακτικά παραδείγματα και χρήσιμες πληροφορίες για να σας βοηθήσει να τους ενσωματώσετε στα έργα σας.

Θυμηθείτε να πειραματιστείτε με αυτούς τους utility types και να εξερευνήσετε πώς μπορούν να χρησιμοποιηθούν για την επίλυση συγκεκριμένων προβλημάτων στον δικό σας κώδικα. Καθώς εξοικειώνεστε περισσότερο μαζί τους, θα διαπιστώσετε ότι τους χρησιμοποιείτε όλο και περισσότερο για να δημιουργήσετε καθαρότερες, πιο συντηρήσιμες και πιο ασφαλείς ως προς τους τύπους εφαρμογές TypeScript. Είτε δημιουργείτε εφαρμογές web, εφαρμογές server-side, ή οτιδήποτε ενδιάμεσο, οι utility types παρέχουν ένα πολύτιμο σύνολο εργαλείων για τη βελτίωση της ροής εργασίας ανάπτυξης και της ποιότητας του κώδικά σας. Αξιοποιώντας αυτά τα ενσωματωμένα εργαλεία χειρισμού τύπων, μπορείτε να ξεκλειδώσετε το πλήρες δυναμικό της TypeScript και να γράψετε κώδικα που είναι τόσο εκφραστικός όσο και στιβαρός.