Здесь будут рассмотрены шесть продвинутых советов по TypeScript, а также примеры, показывающие, как каждый из них работает шаг за шагом, и их преимущества. Используя эти советы в своем собственном коде TypeScript, вы не только повысите общий уровень своего письма, но и улучшите свои навыки программиста TypeScript.

1 — Расширенные типы

Вы можете создавать новые типы на основе существующих, используя расширенные типы TypeScript, такие как сопоставленные типы и условные типы. С помощью этих типов вы можете изменять типы и манипулировать ими, делая ваш код более гибким и удобным в сопровождении.

Сопоставленные типы

Сопоставленные типы перебирают свойства существующего типа и применяют преобразование для создания нового типа. Одним из распространенных вариантов использования является создание версии типа только для чтения.

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

interface Point {
  x: number;
  y: number;
}

type ReadonlyPoint = Readonly<Point>;

В этом примере мы определяем сопоставленный тип с именем Readonly, который принимает тип T в качестве универсального параметра и делает все его свойства доступными только для чтения. Затем мы создаем тип ReadonlyPoint на основе интерфейса Point, где все свойства доступны только для чтения.

Условные типы

Условные типы позволяют создавать новый тип на основе условия. Синтаксис подобен тернарному оператору, использующему ключевое слово extends в качестве ограничения типа.

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

В этом примере мы определяем условный тип с именем NonNullable, который принимает тип T и проверяет, является ли он расширением null или undefined. Если это так, результирующий тип будет never, в противном случае это исходный тип T.

Давайте расширим пример с расширенными типами, включив в него больше удобства использования и выходных данных.

interface Point {
  x: number;
  y: number;
}

type ReadonlyPoint = Readonly<Point>;

const regularPoint: Point = {
  x: 5,
  y: 10
};

const readonlyPoint: ReadonlyPoint = {
  x: 20,
  y: 30
};

regularPoint.x = 15; // This works as 'x' is mutable in the 'Point' interface
console.log(regularPoint); // Output: { x: 15, y: 10 }

// readonlyPoint.x = 25; // Error: Cannot assign to 'x' because it is a read-only property
console.log(readonlyPoint); // Output: { x: 20, y: 30 }

function movePoint(p: Point, dx: number, dy: number): Point {
  return { x: p.x + dx, y: p.y + dy };
}

const movedRegularPoint = movePoint(regularPoint, 3, 4);
console.log(movedRegularPoint); // Output: { x: 18, y: 14 }

// const movedReadonlyPoint = movePoint(readonlyPoint, 3, 4); // Error: Argument of type 'ReadonlyPoint' is not assignable to parameter of type 'Point'

В этом примере мы демонстрируем использование сопоставленного типа Readonly и то, как он обеспечивает неизменяемость. Мы создаем изменяемый объект Point и объект ReadonlyPoint только для чтения. Мы показываем, что попытка изменить свойство, доступное только для чтения, приводит к ошибке времени компиляции. Мы также показываем, что типы только для чтения нельзя использовать там, где ожидаются изменяемые типы, что предотвращает непреднамеренные побочные эффекты в нашем коде.

2 — Декораторы

Декораторы в TypeScript — это мощная функция, позволяющая добавлять метаданные, изменять или расширять поведение классов, методов, свойств и параметров. Это функции более высокого порядка, которые можно использовать для наблюдения, изменения или замены определений классов, определений методов, определений средств доступа, определений свойств или определений параметров.

Декораторы класса

Декораторы класса применяются к конструктору класса и могут использоваться для изменения или расширения определения класса.

function LogClass(target: Function) {
  console.log(`Class ${target.name} was defined.`);
}

@LogClass
class MyClass {
  constructor() {}
}

В этом примере мы определяем декоратор класса с именем LogClass, который регистрирует имя декорированного класса при его определении. Затем мы применяем декоратор к классу MyClass, используя синтаксис @.

Декораторы методов

Декораторы методов применяются к методу класса и могут использоваться для изменения или расширения определения метода.

function LogMethod(target: any, key: string, descriptor: PropertyDescriptor) {
  console.log(`Method ${key} was called.`);
}

class MyClass {
  @LogMethod
  myMethod() {
    console.log("Inside myMethod.");
  }
}

const instance = new MyClass();
instance.myMethod();

В этом примере мы определяем декоратор метода с именем LogMethod, который регистрирует имя декорированного метода при его вызове. Затем мы применяем декоратор к методу myMethod класса MyClass, используя синтаксис @.

Декораторы недвижимости

Декораторы свойств применяются к свойству класса и могут использоваться для изменения или расширения определения свойства.

function DefaultValue(value: any) {
  return (target: any, key: string) => {
    target[key] = value;
  };
}

class MyClass {
  @DefaultValue(42)
  myProperty: number;
}

const instance = new MyClass();
console.log(instance.myProperty); // Output: 42

В этом примере мы определяем декоратор свойства с именем DefaultValue, который устанавливает значение по умолчанию для декорированного свойства. Затем мы применяем декоратор к свойству myProperty класса MyClass, используя синтаксис @.

Декораторы параметров

Декораторы параметров применяются к параметру метода или конструктора и могут использоваться для изменения или расширения определения параметра.

function LogParameter(target: any, key: string, parameterIndex: number) {
  console.log(`Parameter at index ${parameterIndex} of method ${key} was called.`);
}

class MyClass {
  myMethod(@LogParameter value: number) {
    console.log(`Inside myMethod with value ${value}.`);
  }
}

const instance = new MyClass();
instance.myMethod(5);

В этом примере мы определяем декоратор параметров с именем LogParameter, который регистрирует индекс и имя декорированного параметра при вызове метода. Затем мы применяем декоратор к параметру value метода myMethod класса MyClass, используя синтаксис @.

3 — Пространства имен

Пространства имен в TypeScript — это способ организации и группировки связанного кода. Они помогают избежать конфликтов имен и способствуют модульности за счет инкапсуляции кода, который связан друг с другом. Пространства имен могут содержать классы, интерфейсы, функции, переменные и другие пространства имен.

Определение пространств имен

Чтобы определить пространство имен, используйте ключевое слово namespace, за которым следует имя пространства имен. Затем вы можете добавить любой связанный код внутри фигурных скобок.

namespace MyNamespace {
  export class MyClass {
    constructor(public value: number) {}

    displayValue() {
      console.log(`The value is: ${this.value}`);
    }
  }
}

В этом примере мы определяем пространство имен с именем MyNamespace и добавляем в него класс MyClass. Обратите внимание, что мы используем ключевое слово export, чтобы сделать класс доступным за пределами пространства имен.

Использование пространств имен

Чтобы использовать код из пространства имен, вы можете либо использовать полное имя, либо импортировать код с помощью импорта пространства имен.

// Using the fully-qualified name
const instance1 = new MyNamespace.MyClass(5);
instance1.displayValue(); // Output: The value is: 5

// Using a namespace import
import MyClass = MyNamespace.MyClass;

const instance2 = new MyClass(10);
instance2.displayValue(); // Output: The value is: 10

В этом примере мы демонстрируем два способа использования класса MyClass из пространства имен MyNamespace. Во-первых, мы используем полное имя MyNamespace.MyClass. Во-вторых, мы используем оператор импорта пространства имен, чтобы импортировать класс MyClass и использовать его с более коротким именем.

Вложенные пространства имен

Пространства имен могут быть вложены друг в друга для создания иерархии и дальнейшей организации кода.

namespace OuterNamespace {
  export namespace InnerNamespace {
    export class MyClass {
      constructor(public value: number) {}

      displayValue() {
        console.log(`The value is: ${this.value}`);
      }
    }
  }
}

// Using the fully-qualified name
const instance = new OuterNamespace.InnerNamespace.MyClass(15);
instance.displayValue(); // Output: The value is: 15

В этом примере мы определяем вложенное пространство имен с именем InnerNamespace внутри файла OuterNamespace. Затем мы определяем класс MyClass внутри вложенного пространства имен и используем его с полным именем OuterNamespace.InnerNamespace.MyClass.

4 — Миксины

Примеси в TypeScript — это способ составления классов из нескольких более мелких частей, называемых классами примесей. Они позволяют повторно использовать и совместно использовать поведение между различными классами, способствуя модульности и возможности повторного использования кода.

Определение миксинов

Чтобы определить класс миксина, создайте класс, который расширяет параметр универсального типа с помощью сигнатуры конструктора. Это позволяет комбинировать класс mixin с другими классами.

class TimestampMixin<TBase extends new (...args: any[]) => any>(Base: TBase) {
  constructor(...args: any[]) {
    super(...args);
  }

  getTimestamp() {
    return new Date();
  }
}

В этом примере мы определяем класс миксина с именем TimestampMixin, который добавляет метод getTimestamp, который возвращает текущую дату и время. Класс mixin расширяет параметр универсального типа TBase сигнатурой конструктора, чтобы его можно было комбинировать с другими классами.

Использование миксинов

Чтобы использовать класс миксина, определите базовый класс и примените к нему класс миксина, используя ключевое слово extends.

class MyBaseClass {
  constructor(public value: number) {}

  displayValue() {
    console.log(`The value is: ${this.value}`);
  }
}

class MyMixedClass extends TimestampMixin(MyBaseClass) {
  constructor(value: number) {
    super(value);
  }
}

В этом примере мы определяем базовый класс с именем MyBaseClass с методом displayValue. Затем мы создаем новый класс с именем MyMixedClass, который расширяет базовый класс и применяет к нему класс миксина TimestampMixin.

Давайте продемонстрируем, как класс mixin работает на практике.

const instance = new MyMixedClass(42);
instance.displayValue(); // Output: The value is: 42
const timestamp = instance.getTimestamp();
console.log(`The timestamp is: ${timestamp}`); // Output: The timestamp is: [current date and time]

В этом примере мы создаем экземпляр класса MyMixedClass, который включает в себя как метод displayValue из класса MyBaseClass, так и метод getTimestamp из класса миксинов TimestampMixin. Затем мы вызываем оба метода и отображаем их результаты.

5 — Типовая защита

Защита типов в TypeScript — это способ сузить тип переменной или параметра в определенном блоке кода. Они позволяют различать разные типы и получать доступ к свойствам или методам, характерным для этих типов, повышая безопасность типов и снижая вероятность ошибок во время выполнения.

Определение защиты типа

Чтобы определить защиту типа, создайте функцию, которая принимает переменную или параметр и возвращает предикат типа. Предикат типа — это логическое выражение, которое сужает тип параметра в рамках функции.

function isString(value: any): value is string {
  return typeof value === "string";
}

В этом примере мы определяем функцию защиты типа isString, которая проверяет, относится ли заданное значение к типу string. Функция возвращает предикат типа value is string, который сужает тип параметра value в рамках функции.

Использование защиты типа

Чтобы использовать защиту типа, просто вызовите функцию защиты типа в условном операторе, таком как оператор if или оператор switch.

function processValue(value: string | number) {
  if (isString(value)) {
    console.log(`The length of the string is: ${value.length}`);
  } else {
    console.log(`The square of the number is: ${value * value}`);
  }
}

В этом примере мы определяем функцию с именем processValue, которая принимает значение типа string | number. Мы используем функцию защиты типа isString, чтобы проверить, является ли значение строкой. Если это так, мы получаем доступ к свойству length, характерному для типа string. В противном случае мы предполагаем, что значение равно number, и вычисляем его квадрат.

Давайте продемонстрируем, как работает type guard на практике.

processValue("hello"); // Output: The length of the string is: 5
processValue(42); // Output: The square of the number is: 1764

В этом примере мы вызываем функцию processValue со строкой и числом. Функция защиты типа isString обеспечивает выполнение соответствующего блока кода для каждого типа, что позволяет нам получать доступ к специфичным для типа свойствам и методам без каких-либо ошибок типа.

6 — Типы утилит

Служебные типы в TypeScript предоставляют удобный способ преобразования существующих типов в новые типы. Они позволяют создавать более сложные и гибкие типы без необходимости определять их с нуля, способствуя повторному использованию кода и безопасности типов.

Использование служебных типов

Чтобы использовать служебный тип, примените служебный тип к существующему типу, используя синтаксис угловых скобок. TypeScript предоставляет множество встроенных типов утилит, таких как Partial, Readonly, Pick и Omit.

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

type PartialPerson = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;
type NameAndAge = Pick<Person, "name" | "age">;
type WithoutEmail = Omit<Person, "email">;

В этом примере мы определяем интерфейс с именем Person с тремя свойствами: name, age и email. Затем мы используем различные встроенные типы утилит для создания новых типов на основе интерфейса Person.

Давайте продемонстрируем, как служебные типы работают на практике.

Частично:

const partialPerson: PartialPerson = {
  name: "John Doe",
};

В этом примере мы создаем объект partialPerson типа PartialPerson. Тип утилиты Partial делает все свойства интерфейса Person необязательными, что позволяет нам создать частичную персону только со свойством name.

Только чтение:

const readonlyPerson: ReadonlyPerson = {
  name: "Jane Doe",
  age: 30,
  email: "[email protected]",
};

// readonlyPerson.age = 31; // Error: Cannot assign to 'age' because it is a read-only property

В этом примере мы создаем объект readonlyPerson типа ReadonlyPerson. Тип утилиты Readonly делает все свойства интерфейса Person доступными только для чтения, не позволяя нам изменять свойство age.

Выбрать:

const nameAndAge: NameAndAge = {
  name: "John Smith",
  age: 25,
};

// nameAndAge.email; // Error: Property 'email' does not exist on type 'Pick<Person, "name" | "age">'

В этом примере мы создаем объект nameAndAge типа NameAndAge. Тип утилиты Pick создает новый тип только с указанными свойствами интерфейса Person, в данном случае name и age.

Пропустить:

const withoutEmail: WithoutEmail = {
  name: "Jane Smith",
  age: 28,
};

// withoutEmail.email; // Error: Property 'email' does not exist on type 'Omit<Person, "email">'

В этом примере мы создаем объект withoutEmail типа WithoutEmail. Тип утилиты Omit создает новый тип, удаляя указанные свойства из интерфейса Person, в данном случае email.

В заключение в этой статье были рассмотрены различные расширенные темы TypeScript, такие как пространства имен, расширенные типы, декораторы, примеси, защита типов и служебные типы. Понимая и используя эти функции, вы можете создавать более модульный, повторно используемый и удобный в сопровождении код, который соответствует лучшим практикам и снижает вероятность ошибок во время выполнения.

Используя эти расширенные функции TypeScript, вы можете писать более чистый, организованный и удобный для сопровождения код, который в полной мере использует мощную систему типов и языковые функции TypeScript.

Если вам понравилась эта статья и вы нашли ее полезной, не стесняйтесь ознакомиться с другой моей статьей 12 — Советы по TypeScript для чистого кода. Расширьте свои знания TypeScript и улучшите свои навыки кодирования, изучив дополнительные советы и приемы!