TypeScript 5.5 Що нового.

TypeScript

Предикати типу, що виводиться

Аналіз потоку управління в 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,

Джерело