Інтерфейс в TypeScript — це спосіб опису форми об’єкта. Він визначає, які властивості та методи повинен мати об’єкт, але не містить їх реалізації. Це своєрідний контракт, якого повинен дотримуватись об’єкт.

В цьому записі буде розглянуто:

Створення та використання інтерфейсу

Розглянемо простий приклад інтерфейсу:

interface Person {
  name: string;
  age: number;
  info(): void;
}

Інтерфейс Person описує об’єкт, який повинен мати дві властивості: name (тип string) та age (тип number). Також об’єкт повинен реалізувати метод info(), який не приймає параметрів та повертає void.

Використання інтерфейсу для створення об’єкту:

let employee: Person = {
  name: 'Oliviero Lorenzo',
  age: 30,
  info: function (): void {
    console.log(`Employee name: ${this.name}, age: ${this.age}`);
  },
};

employee.info();

// OUTPUT: Employee name: Oliviero Lorenzo, age: 30

Об’єкт employee реалізує інтерфейс Person, що накладає на нього деякі обмеження. employee повинен мати всі властивості та методи, визначені в інтерфейсі Person (name, age та info).

Приклад використання інтерфейсу для типізації параметрів функцій:

interface User {
  name: string;
  age: number;
}

let user: User = {
  name: 'Samson Mcgrath',
  age: 30,
};

function greet(user: User): void {
  console.log(`Hello, ${user.name}!`);
}

greet(user);

// OUTPUT: Hello, Samson Mcgrath!

В цьому випадку аргумент, котрий передається у функцію greet, має представляти об’єкт або клас, який реалізує інтерфейс User.

Приклад використання інтерфейсу для типізації значення що повертається функцією:

interface User {
  name: string;
  age: number;
}

function buildUser(name: string, age: number): User {
  return { name, age };
}

let user: User = buildUser('Jimmy Shields', 28);
console.log(user.name);

// OUTPUT: Jimmy Shields

В цьому прикладі функція buildUser повертає об’єкт, який реалізує інтерфейс User.

Опціональні властивості інтерфейсів

Під час визначення інтерфейсу, можна задати деякі властивості як необов’язкові, за допомогою знаку запитання (?). Подібні властивості необов’язкові для додавання в об’єкт:

interface Person {
    name: string;
    age: number;
    job?: string;
}

let employee: Person = {
    name: 'Jaime Patel',
    age: 23,
    job: 'manager',
}

let manager: Person = { 
    name: 'Arron Mayer',
    age: 31,
}

Властивість job інтерфейсу Person позначено як необов’язкову, тому її можна не визначати в об’єктах.

Readonly властивості

Інтерфейс також може визначати властивості, які не можна змінювати після встановлення їх значень. Для цього використовується ключове слово readonly:

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

let user: User = { id: 1, name: 'Eric' };

user.name = 'Patrick'; // Допустимо
user.id = 2; // Cannot assign to 'id' because it is a read-only property.

Визначення методів

Окрім властивостей інтерфейси можуть визначати методи:

interface Person {
  name: string;
  sayWords(words: string): void;
}

let employee: Person = {
  name: 'Bruno Riddle',
  sayWords: function (words: string): void {
    console.log(`${this.name} say ${words}`);
  },
};

employee.sayWords('Hello');

// OUTPUT: Bruno Riddle say Hello

В прикладі вище інтерфейс Person визначає метод sayWords, котрий приймає один аргумент типу string та нічого не повертає. Об’єкт що реалізує інтерфейс, також зобов’язаний реалізувати визначені в інтерфейсі методи, з тими самими наборами параметрів та типами вихідного результату.

Інтерфейси класів

Інтерфейси можуть бути реалізовані не тільки об’єктами, а й класами. Для цього в оголошенні класу слід використовувати ключове слово implements:

interface Person {
  firstName: string;
  lastName: string;
  getFullName(): string;
}

class Employee implements Person {
  firstName: string;
  lastName: string;

  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
  getFullName(): string {
    return `${this.firstName} ${this.lastName.toUpperCase()}`;
  }
}

Клас Employee реалізує інтерфейс Person, тож зобов’язаний визначити всі властивості та методи, які є в Person.

При цьому змінну, що посилається на екземпляр класу Employee, можна типізувати як Employee:

let employee: Employee = new Employee('Rajan', 'Hardy');
console.log(employee.getFullName());

Так і Person:

let person: Person = new Employee('Rajan', 'Hardy');
console.log(person.getFullName());

Розширення інтерфейсу

TypeScript дає змогу додавати до інтерфейсу нові поля та методи, просто оголосивши інтерфейс із тим самим ім’ям, і визначивши в ньому необхідні властивості і методи. Наприклад:

interface Person {
  name: string;
}

interface Person {
  age: number;
}

let employee: Person = {
  name: 'Paula',
  age: 31,
};

function printPerson(person: Person): void {
  console.log(`name: ${person.name}  age: ${person.age}`);
}

printPerson(employee);

// OUTPUT: name: Paula  age: 31

У цьому прикладі перше визначення інтерфейсу Person містить властивість name, друге містить властивість age. В підсумку, об’єкт або клас котрі реалізують цей інтерфейс, мають визначити обидві властивості – name та age.

Успадкування інтерфейсів

Інтерфейси, як і класи, можуть успадковуватись:

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

interface Employee extends Person {
  job: string;
  printEmployeeInfo(): void;
}

class User implements Employee {
  name: string;
  age: number;
  job: string;

  constructor(name: string, age: number, job: string) {
    this.name = name;
    this.age = age;
    this.job = job;
  }

  printEmployeeInfo(): void {
    console.log(`name: ${this.name}, age: ${this.age}, job: ${this.job}`);
  }
}

let user: User = new User('Leyla Huber', 30, 'manager');
user.printEmployeeInfo();

// OUTPUT: name: Leyla Huber, age: 30, job: manager

Інтерфейс Employee успадковує всі властивості та методи, які визначені в Person. Клас User, котрий реалізує інтерфейс Employee, повинен визначити властивості та методи обох інтерфейсів (Person та Employee).

Інтерфейси функцій

Інтерфейси можуть описувати не тільки об’єкти. Приклад використання інтерфейсу для опису функції:

interface Formatter {
  (text: string, toUpper: boolean): string;
}

let formatter: Formatter = function (text: string, toUpper: boolean): string {
  if (toUpper) {
    return text.toUpperCase();
  }

  return text.toLowerCase();
};

let formattedText: string = formatter('text', true);
console.log(formattedText);

// OUTPUT: TEXT

В цьому прикладі інтерфейс Formatter містить сигнатуру функції, яка приймає два аргументи (string та boolean типів), та повертає значення типу string. Змінна formatter має тип інтерфейсу Formatter, тому має представляти функцію з визначеною в інтерфейсі сигнатурою.

Інтерфейс об’єкту що індексується

Інтерфейси можуть використовуватися для опису типів, котрі індексуються (наприклад obj[1] або obj[‘a’]), де можна визначати тип індексу та тип значення:

interface StringArray {
  [index: number]: string;
}

let names: StringArray = ['Bob', 'Fred'];
let name: string = names[0];

Об’єкт що реалізує інтерфейс StringArray, має зберігати об’єкти типу string, котрі індексуються за допомогою number.

Гібридні інтерфейси

TypeScript підтримує гібридні інтерфейси, тобто коли інтерфейс застосовується для опису об’єкту, котрий діє і як функція, і як об’єкт:

interface Counter {
  (start: number): void;
  start: number;
  interval: number;
  reset(): void;
}

function getCounter(): Counter {
  let counter: Counter = <Counter>function (start: number) {
    console.log(`Start conuter with value: ${start}`);
    counter.start = start;
  };
  counter.interval = 10;
  counter.reset = function (): void {
    console.log('Reset counter');
  };

  return counter;
}

let counter: Counter = getCounter();
counter(1); // OUTPUT: Start conuter with value: 1
console.log(`Counter interval: ${counter.interval}`); // OUTPUT: Counter interval: 10
counter.reset(); // OUTPUT: Reset counter

Generic інтерфейси

Generic (узагальнені) інтерфейси, дозволяють створювати інтерфейси, які споживають узагальнений тип та призначають його одній або декільком властивостям, визначеним в інтерфейсі:

interface Item<T> {
  value: T;
}

let stringItem: Item<string> = { value: 'Text' };
let numberItem: Item<number> = { value: 10 };

Переваги використання інтерфейсів у TypeScript

  • Перевірка типів – інтерфейси висвітлюють потенційні помилки типу.
  • Визначення контракту – інтерфейси визначають і створюють чіткі контракти для функцій і коду, які їх споживають. Запобігають спробам використання неіснуючих властивостей та методів, допомагаючи залишатись в межах встановленої структури, визначеної для об’єкта котрий описано в інтерфейсі.
  • Документація та читабельність – оскільки інтерфейси визначають властивості та методи, котрі має об’єкт, а також їхні типи, вони виступають формою документації для покращення читабельності коду.
  • Повторне використання – інтерфейси можна розширювати і повторно використовувати в різних місцях, що сприяє повторному використанню коду та допомагає зменшити дублювання.
  • Навігація кодом та автозавершення – IDE, які інтегруються з TypeScript, можуть читати визначені вами інтерфейси і пропонувати варіанти щодо автозавершення, а також допомагати з навігацією по коду.
  • Легший рефакторинг – інтерфейси полегшують рефакторинг, оскільки можна оновити реалізацію частини коду, і поки він продовжує дотримуватись того самого інтерфейсу, залежний код не повинен зазнати впливу.

Мітки: