Предисловие

Всем привет, меня зовут Сергей, в этой статье я опубликую свой перевод официального анонса релиза TypeScript 5.5 версии, спасибо Dan Vanderkam за оригинал. Опыта в написании статей ранее не имел, переводы тоже не делал, решился внести свою лепту в сообщество Хабра. Открыт к критике, если первая часть понравится и в комментариях я увижу интерес к продолжению, то займусь выпуском следующих частей.

В первой части предлагаю ознакомиться с предикатами выводимого типа и то как всё это поменялось в TypeScript 5.5 версии, приступим!

Предикаты выводимого типа

Анализ потока управления в TypeScript отлично справляется с отслеживанием, того как изменяется тип переменной по мере перемещения по коду, давайте рассмотрим на примере:

interface Bird {
    commonName: string;
    scientificName: string;
    sing(): void;
}
// Map содержит в себе: название страны -> национальная птица
// Не во всех странах есть национальные птицы(привет, Канада!)
declare const nationalBirds: Map<string, Bird>;
function makeNationalBirdCall(country: string) {
  const bird = nationalBirds.get(country);  // У bird есть объявленный тип Bird | undefined
  if (bird) {
    bird.sing();  // если переменная bird имеет тип Bird
  } else {
    // если переменная bird имеет тип undefined
  }
}

Из-за того, что тип переменной бывает undefined, TypeScript заставляет Вас проверять и обрабатывать такие случаи, тем самым подталкивая Вас писать более надёжный и защищённый код.

А теперь посмотрим на работу с массивами в коде ниже, раньше это было бы ошибкой во всех предыдущих версиях TypeScript:

function makeBirdCalls(countries: string[]) {
  // birds: (Bird | undefined)[]
  const birds = countries
    .map(country => nationalBirds.get(country))
    .filter(bird => bird !== undefined);
  for (const bird of birds) {
    bird.sing();  // error: 'bird' is possibly 'undefined'.
  }
}

Несмотря, на то, что мы отфильтровали все значения undefined, тем не менее TypeScript не смог это отследить и выдал ошибку.

В TypeScript версии 5.5, больше таких проблем нет, проверка типов отлично справляется с этим кодом:

function makeBirdCalls(countries: string[]) {
  // birds: Bird[]
  const birds = countries
    .map(country => nationalBirds.get(country))
    .filter(bird => bird !== undefined);
  for (const bird of birds) {
    bird.sing();  // ok!
  }
}

Это работает, потому что TypeScript теперь выводит предикат типа для функции filter. Вы можете увидеть, что происходит, более ясно, выделив это в отдельную функцию:

// function isBirdReal(bird: Bird | undefined): bird is Bird
function isBirdReal(bird: Bird | undefined) {
  return bird !== undefined;
}

bird is Bird — это предикат типа. Это означает, что если функция возвращает true, то это Bird (если функция возвращает false, то он не определен, то есть undefined). Объявления типов для Array.prototype.filter знают о предикатах типа, поэтому в конечном итоге вы получаете более точный тип, и код проходит проверку типов.

TypeScript сделает вывод, что функция возвращает предикат типа, если выполняются следующие условия:

  1. Функция не имеет явного возвращаемого типа или аннотации предиката типа.

  2. Функция имеет один оператор return и неявных возвратов нет.

  3. Функция не изменяет свой параметр.

  4. Функция возвращает логическое выражение, привязанное к уточнению параметра.

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

// const isNumber: (x: unknown) => x is number
const isNumber = (x: unknown) => typeof x === 'number';
// const isNonNullish: <T>(x: T) => x is NonNullable<T>
const isNonNullish = <T,>(x: T) => x != null;

Раньше TypeScript просто выводил, что эти функции возвращают boolean. Теперь он выводит сигнатуры с предикатами типа, например, x is number или x is NonNullable<T>.

Предикаты типа имеют семантику “if and only if”. Если функция возвращает x is T, то это означает, что:

  1. Если функция возвращает true, то x имеет тип T.

  2. Если функция возвращает false, то x не имеет типа T.

Если вы ожидаете, что предикат типа будет выведен, но этого не происходит, то вы можете нарушить второе правило. Это часто приводит к проверкам «истинности»:

function getClassroomAverage(students: string[], allScores: Map<string, number>) {
  const studentScores = students
    .map(student => allScores.get(student))
    .filter(score => !!score);
  return studentScores.reduce((a, b) => a + b) / studentScores.length;
  //     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // error: Object is possibly 'undefined'.
}

TypeScript не вывел предикат типа для score => !!score, и это правильно: если это возвращает true, то score — это number. Но если это возвращает false, то score может быть либо undefined, либо number(в частности, 0). Это реальный баг: если какой-либо студент получил нулевой балл в тесте, то фильтрование его баллов исказит средний балл вверх.

Как и в первом примере, лучше явно отфильтровать undefined значения:

function getClassroomAverage(students: string[], allScores: Map<string, number>) {
  const studentScores = students
    .map(student => allScores.get(student))
    .filter(score => score !== undefined);
  return studentScores.reduce((a, b) => a + b) / studentScores.length;  // ok!
}

Проверка истинности выведет предикат типа для типов объектов, где нет неоднозначности. Помните, что функции должны возвращать boolean значение, чтобы быть кандидатом на выведенный предикат типа: x => !!x может вывести предикат типа, но x => x определенно не будет.

Явные предикаты типа продолжают работать точно так же, как и раньше. TypeScript не будет проверять, выведет ли он тот же предикат типа. Явные предикаты типа («is») не безопаснее утвержде��ия типа («as»).

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

// Раньше, nums: (number | null)[]
// Сейчас, nums: number[]
const nums = [1, 2, 3, null, 5].filter(x => x !== null);
nums.push(null);  // ok в TS 5.4, error в TS 5.5

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

const nums: (number | null)[] = [1, 2, 3, null, 5].filter(x => x !== null);
nums.push(null);  // ok, теперь этот код работает так же как в старых версиях

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