5 первоклассных приемов TypeScript, которые вы ДОЛЖНЫ знать

TypeScript мощнее, чем просто интерфейсы

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

Но этот мем суммирует мой опыт:

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

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

Типизированное возвращаемое значение в соответствии с параметром

type GetUsernameAdditionalArgs = { required?: boolean };
type GetUsernameReturnValue<T extends GetUsernameAdditionalArgs> =
  T["required"] extends true ? string : string | null;

function getUsername<T extends GetUsernameAdditionalArgs>(
  id: number,
  additionalArgs?: T
): GetUsernameReturnValue<T> {
  // get username
  const username = "...";

  if (additionalArgs?.required && username == null) {
    throw Error("");
  }

  return username as GetUsernameReturnValue<T>;
}

// username will be of type string | null
const username = getUsername(10);

// requiredUsername will be of type string
const requiredUsername = getUsername(10, { required: true });

Это очень полезный навык для применения аннотаций типов к функциям, которые могут возвращать значение null или undefined. Например, если у вас есть функция с именем getUsername, которая извлекает данные из внешнего API, вы можете добавить блок if для выдачи ошибки, если требуется имя пользователя.

Самое приятное то, что вы также можете отключить блок try-catch по мере необходимости, что дает вам больше контроля над поведением функции.

Расширьте интерфейс, чтобы сделать поля необязательными

interface User {
  username: string;
  userId: number;
  description?: string;
}

interface UserWithOptionalUsername extends Omit<User, 'username'> {
  username?: string;
}

const user: UserWithOptionalUsername = {
  userId: 10,
};

Иногда нам может понадобиться сделать определенные поля необязательными в наших интерфейсах TypeScript. Для этого мы можем расширить тип утилиты Omit из нашего базового интерфейса и переопределить поле со знаком вопроса (?), чтобы сделать его необязательным.

Расширьте интерфейс, чтобы сделать поля обязательными

interface User {
  username: string;
  userId: number;
  description?: string;
}

interface UserWithDescription extends User {
  description: string;
}

const userWithDescription: UserWithDescription = {
  userId: 10,
  username: "",
  description: "",
};

В некоторых сценариях нам может потребоваться сделать определенные свойства обязательными в наших интерфейсах TypeScript. Для этого мы можем расширить базовый интерфейс и переопределить свойство без знака вопроса (?), чтобы сделать его обязательным.

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

namespace MyService {
  export function findUser() {}
}

const user = MyService.findUser();

namespace в TypeScript не используется широко, потому что мы можем просто использовать именованный импорт:

import * as Module from '.'

console.log(Module.myVar);

Однако namespace может обеспечить простой и строгий способ организации кода в логические группы, в отличие от module.

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

Типизированный switch-case без значений по умолчанию

// initial code
type Status = "running" | "pending" | "success";

function execByStatus(status: Status): number {
  switch (status) {
    case "running":
      return 1;
    case "pending":
      return 2;
    case "success":
      return 3;
  }
}

// =================================================================== //
// modified Status type
type Status = "running" | "pending" | "success" | "stuck";

// will give the error: Function lacks ending return statement and 
// return type does not include 'undefined'
// error catched during compilation!!
function execByStatus(status: Status): number {
  switch (status) {
    case "running":
      return 1;
    case "pending":
      return 2;
    case "success":
      return 3;
  }
}

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

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

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

Дополнительно: требуется TypeScript, выбрать, пропустить и частично

Вот несколько сценариев, в которых вам может понадобиться реализовать некоторые из самых мощных типов в TypeScript:

Базовый интерфейс

// Base interface 
interface User {
  email: string;
  username: string;
  description: string;
}

Требуется‹T›

// All properties in User is now required 
type UserWithDescription = Required<User>;
const userWithDescription: UserWithDescription = {
  email: "",
  username: "",
  description: "",
};

Required<T> — это встроенный служебный тип в TypeScript, который создает новый тип, делая все свойства исходного типа T обязательными.

Выберите‹Т›

// Only pick a few properties from base interface
type UserWithoutDescriptionV1 = Pick<User, "email" | "username">;
const userWithDescriptionV1: UserWithoutDescriptionV1 = {
  email: "",
  username: "",
};

Pick<T, K> — это встроенный служебный тип в TypeScript, который создает новый тип, выбирая набор свойств K из исходного типа T. Используйте |, чтобы добавить больше ключей.

Пропустить‹T›

// Only pick a few properties from base interface
type UserWithoutDescriptionV2 = Omit<User, "description">;
const userWithDescriptionV2: UserWithoutDescriptionV2 = {
  email: "",
  username: "",
};

Omit<T, K> — это встроенный служебный тип в TypeScript, который создает новый тип, опуская набор свойств K из исходного типа T.

Частичное‹T›

type IncompleteUser = Partial<User>;
const incompleteUser: IncompleteUser = {};

Partial<T> — это встроенный служебный тип в TypeScript, который создает новый тип, делая все свойства исходного типа T необязательными.

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