Предисловие

Всем привет, меня зовут Сергей, в этой статье я опубликую свой перевод официального анонса релиза 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, теперь этот код работает так же как в старых версиях

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

Комментарии (3)


  1. NeoCode
    31.07.2024 08:02

    Typescript судя по всему интереснейший язык, кажется там наиболее полно реализована система типов именно с теоретической точки зрения (но я до его серьезного изучения всё никак не доберусь).


    1. meonsou
      31.07.2024 08:02
      +3

      кажется там наиболее полно реализована система типов именно с теоретической точки зрения

      Даже не близко в этом плане, хотя конечно смотря с чем сравнивать. Можете посмотреть на всякие proof-oriented языки типа F* и Lean чтобы увидеть теорию типов в действии.


  1. meonsou
    31.07.2024 08:02

    Предикаты типа имеют семантику “if and only if”

    Это буквально переводится как "тогда и только тогда"