TypeScript — это надмножество JavaScript, добавляющее статические типы к динамическому языку. Одной из особенностей, которая отличает TypeScript от JavaScript, является поддержка сужения типов, что позволяет вам получать доступ к свойствам и методам, доступным только для определенных типов, а также помогает TypeScript обнаруживать ошибки и ошибки во время компиляции.

Что такое сужение типа?

Сужение типа — это процесс уточнения типа переменной на основе условия. Это может быть полезно, когда у вас есть переменная, которая может иметь несколько возможных типов, но вы хотите выполнять над ней операции, допустимые только для определенного типа.

TypeScript использует анализ потока управления для сужения типов на основе условных операторов, циклов и проверок достоверности. Сужение типа обычно выполняется с помощью условных операторов, таких как операторы if или операторы switch.

function printLength(strOrArray: string | string[]) {
  if (typeof strOrArray === "string") {
    console.log(strOrArray.length);
  } else {
    console.log(strOrArray.length);
  }
}

printLength("hello"); // prints 5
printLength(["hello", "world"]); // prints 2

Вот пример использования операторов switch:

interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      throw new Error(`Invalid shape: ${shape}`);
  }
}

const circle: Circle = { kind: "circle", radius: 5 };
const square: Square = { kind: "square", sideLength: 4 };

console.log(getArea(circle)); // prints 78.53981633974483
console.log(getArea(square)); // prints 16

Сужение типа против приведения типа

Сужение типов и приведение типов — связанные, но разные концепции.

Сужение типа — это процесс уточнения значения нескольких типов в один конкретный тип на основе некоторого условия или проверки.

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

Например, в TypeScript вы можете использовать as для приведения значения к другому типу:

let x: any = "hello";
let y = x as string; // cast x to string

Это пример приведения типов, но не сужения типа, поскольку x по-прежнему имеет тип any после приведения. Чтобы сузить x до строки, вам нужно использовать защиту типа:

let x: any = "hello";
if (typeof x === "string") {
  // x is narrowed to string
  let y = x; // y is string
}

Одно ключевое различие между сужением типов и приведением типов заключается в том, что сужение типов всегда безопасно для типов, а это означает, что система типов гарантирует, что суженный тип является допустимым подтипом исходного типа. Приведение типов, с другой стороны, не всегда безопасно для типов и может привести к ошибкам во время выполнения, если приводимое значение на самом деле не имеет ожидаемого типа. Например:

function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    // padding is narrowed to number
    return " ".repeat(padding) + input;
  }
  // padding is narrowed to string
  return padding + input;
}

function padRight(padding: number | string, input: string) {
  return input + (padding as string); // cast padding to string
}

let x: number | string = Math.random() < 0.5 ? 10 : "hello";

console.log(padLeft(x, "world")); // works fine
console.log(padRight(x, "world")); // may throw an error

Функция padRight использует приведение типов для преобразования padding в строку независимо от ее фактического типа. Это может вызвать ошибку во время выполнения, если padding на самом деле является числом, так как числа не имеют метода toString.

Еще одно отличие состоит в том, что сужение типа можно выполнить без изменения типа переменной или выражения, тогда как приведение типов всегда требует изменения типа значения. Это означает, что сужение типа обычно является более легкой и менее навязчивой операцией, чем приведение типа.

Распространенные способы сузить тип

TypeScript предоставляет различные способы создания защиты типов, которые представляют собой выражения, выполняющие проверку во время выполнения, гарантирующую тип в некоторой области. Некоторыми из встроенных охранников типов являются typeof, instanceof и in, но мы также можем создавать собственные пользовательские функции защиты типов, используя предикаты типов.

(1) Использование оператора typeof, который возвращает строку, представляющую тип значения.

function printValue(value: string | number) {
  if (typeof value === "string") {
    console.log(`The string is ${value}`);
  } else {
    console.log(`The number is ${value}`);
  }
}

printValue("hello"); // prints "The string is hello"

(2) Использование оператора instanceof, который проверяет, является ли объект экземпляром определенного класса или функции-конструктора.

class Animal {
  speak() {
    console.log("The animal speaks");
  }
}

class Dog extends Animal {
  bark() {
    console.log("The dog barks");
  }
}

function callSpeakOrBark(animal: Animal) {
  if (animal instanceof Dog) {
    animal.bark();
  } else {
    animal.speak();
  }
}

const animal = new Animal();
const dog = new Dog();
callSpeakOrBark(animal); // prints "The animal speaks"
callSpeakOrBark(dog); // prints "The dog barks"

(3) Использование оператора in, который возвращает true, если имя свойства или индекс присутствует в объекте или массиве.

interface Cat {
  name: string;
  meow(): void;
}

interface Dog {
  name: string;
  bark(): void;
}
type Pet = Cat | Dog;
function greet(pet: Pet) {
  if ("meow" in pet) {
    // pet is narrowed to Cat
    pet.meow();
  } else {
    // pet is narrowed to Dog
    pet.bark();
  }
}

(4) Использование определяемой пользователем защиты типа, которая представляет собой функцию, возвращающую логическое значение и имеющую предикат типа в качестве возвращаемого типа. Предикат типа — это выражение, имеющее форму parameterName is Type, где parameterName должно быть именем параметра из текущей сигнатуры функции.

interface Fish {
  swim(): void;
}

interface Bird {
  fly(): void;
}

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    // pet is narrowed to Fish
    pet.swim();
  } else {
    // pet is narrowed to Bird
    pet.fly();
  }
}