Типовые предикаты – это интересная синтаксическая особенность в TypeScript.尽管它们出现在与 возвращаемого типа аннотации相同的位置,但 они выглядят более как короткие положительные предложения, чем традиционные аннотации. Это gives you greater control over type checking.

С выпуском TypeScript 5.5 работа с типовыми предикатами стала более интуитивной, теперь он может их автоматически идентифицировать в многих случаях. However, if you’re working with slightly older code-bases, you’re likely to encounter handwritten type predicates more often.

В этой статье мы briefly explore what type predicates are and why they are useful. Let’s start by looking at the problem they solve.

The Problem

The best way to understand the usefulness of type predicates, I believe, is by noticing the problems that arise when we don’t have them:

function isString(value: unknown): boolean {
  return typeof value === "string";
}

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // string | number
  }
  return " ".repeat(padding) + input; // Opps type error here
                 //   ^
                 // string | number
}

Here, the return type of isString is set to boolean, and we use it in a function called padLeft to add padding to the left of an input string. The padding can be either a given string or a specified number of space characters.

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

Функция будет исполняться без проблем во время выполнения, но TypeScript не сможет выполнять сужение типов с помощью isString. В результате тип padding остается string | number как внутри, так и снаружи оператора if. Это приводит к конфликту с ожиданием первого аргумента у repeat и вызывает ошибку типа.

Решение: Введение типовых предикатов

Даже если вы не знакомы с термином “предикат”, вы, вероятно, уже использовали их раньше. Предикаты в программировании – это просто функции, которые возвращают булево значение в ответ на вопрос да/нет. Несколько встроенных методов массивов JavaScript, таких как filter, find, every и some, используют предикаты для принятия решений.

Типовые предикаты позволяют сделать предикаты более полезными для сужения типов. Мы можем исправить проблему, используя типовый предикат в качестве типа возвращаемого значения:

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

Здесь типовый предикат – это value is string. Он говорит три вещи:

  • Функция является предикатом. Поэтому TypeScript покажет ошибку, если вы попытаетесь вернуть что-то, кроме булевого значения.

  • Если она возвращает true, то value является строкой.

  • Если она возвращает false, то value не является строкой.

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

Полный код здесь:

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

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // string
  }
  return " ".repeat(padding) + input;
                 //   ^
                 // number
}

В этом месте TypeScript правильно сужает тип padding внутри утверждения if и за его пределами.

Теперь вкратце посмотрим, как работают типовые предicate до версии TypeScript 5.5 и какие улучшения этот выпуск внес.

Типовые Predicates до TypeScript 5.5

В нашем предыдущем примере, если мы не указываем никакого return type, он будет расположен как boolean:

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

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // string | number
  }
  return " ".repeat(padding) + input; // Opps type error here
                 //   ^
                 // string | number
}

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

Если мы не указываем предложения о типе, использование методов, таких как filter, также может привести к неверному обнаружению типа:

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

const numsOrStrings = [1, 'hello', 2, 'world'];
//      ^
//    strings: (string | number)[]

const strings = numsOrStrings.filter(isString);
//      ^
//    strings: (string | number)[]

Теперь посмотрим, как TypeScript 5.5 улучшает ситуацию.

Предложения о типе после TypeScript 5.5

Одна из главных особенностей TypeScript 5.5 заключается в том, что он может определять предложения о типе путем анализа тела функции. Так что если вы используете TypeScript 5.5 или новее, вам не нужно писать предложения о типе в качестве return type для isString. TypeScript это делает за вас, и код такого вида, как увиден в примере ниже, работает отлично:

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

function padLeft(padding: number | string, input: string) {
  if (isString(padding)) {
    return padding + input;
        //   ^
        // строка
  }
  return " ".repeat(padding) + input; // Ошибка типа Opps здесь
                 //   ^
                 // число
}

const numsOrStrings = [1, 'hello', 2, 'world'];

const strings = numsOrStrings.filter(isString);
//      ^
//    строки: string[]

const numbers = numsOrStrings.filter((v) => !isString(v));
//      ^
//    числа: number[]

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

Дальнейшее изучение

В этой статье мы кратко рассмотрели предикаты типов в TypeScript. Если вас интересует изучение подробностей и понимание крайних случаев, вот официальные руководства:

Спасибо за чтение! С уважением, следующий раз!

Фоновое изображение для обложки за имённо Mona Eendra на Unsplash.