Предисловие
Всем привет, меня зовут Сергей, в этой статье я опубликую свой перевод официального анонса релиза 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 сделает вывод, что функция возвращает предикат типа, если выполняются следующие условия:
Функция не имеет явного возвращаемого типа или аннотации предиката типа.
Функция имеет один оператор return и неявных возвратов нет.
Функция не изменяет свой параметр.
Функция возвращает логическое выражение, привязанное к уточнению параметра.
В целом это работает так, как и ожидалось. Вот еще несколько примеров предикатов выводимых типов:
// 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, то это означает, что:
Если функция возвращает true, то x имеет тип T.
Если функция возвращает 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, теперь этот код работает так же как в старых версияхДля получения дополнительной информации ознакомьтесь с запросом на внедрение и записью в блоге Дэна о внедрении этой функции, а так же оригинал статьи.
