Как стать автором
Обновить

Система типов и настройки

Уровень сложностиСредний
Время на прочтение2 мин
Количество просмотров1.6K

Предисловие

Написать заметку меня побудила статья Как устроена система типов typescript и собственный опыт.

Я обратил внимание, что не всегда понимал семантику "extends" в разных контекстах и влияние настроек языка.

В результате, то что меня смущало, оказалось рабочим поведением языка, но при этом непредсказуемым и небезопасным.

Введение

Для демонстрации я выбрал простое объединение string | number, которое буду помещать в различные контексты.

Пример 1:

type TA = number | string
type TB = string

type E1 = TA extends TB ? true : false // false
type E2 = TB extends TA ? true : false // true

export const example1 = () => {
  let a: TA = 100
  let b: TB = 'value 100'

  b = a
  a = b

  return true
}

Когда мы имеем дело с примитивными типами наше объединение будет выступать в определенном смысле супер типом для string или number.
И по описанному в статье, в примере 1 мы будем иметь некорректное нисходящее приведение b = a и корректное восходящее приведение a = b.
Пока все понятно, и предсказуемо.

Функции

Вариант 2:

type TC = (a: number | string) => void
type TD = (a: string) => void

type E3 = TC extends TD ? true : false // true 
type E4 = TD extends TC ? true : false // true

let c: TC = a => undefined
let d: TD = a => {
  if (typeof a !== 'string') {
    throw 'error a in d'
  }
}

const example2 = () => {
  c = d
  d = c

  return true
}

Вроде по смыслу ничего не поменялось, но ts перестал работать как ожидаем. Здесь у нас работает явно противоречащие принципу небезопасное присваивание.
За такие финты ушами отвечает compilerOptions->strict установленный в true. А так же может быть использована отдельная опция strictFunctionTypes.
Включение этих флажков решает проблему и мы получаем предупреждение о небезопасном присваивании.

Объекты

Адаптируем функцию выше как метод объекта.

Вариант 3:

interface IA {
  setValue(a: number | string): void
}

class CB implements IA {
  setValue(a: string) {
    if (typeof a !== 'string') {
      throw 'error a in CB'
    }
  }
}

const example3 = () => {
  const oIA: IA = new CB
  oIA.setValue(100) // Error: thrown: "error a in CB"

  return true
}

Наш конфиг уже имеет флаг strict->true. Но никаких ошибок вида ниже мы не получаем:

TS2322: Type TD is not assignable to type TC
  Types of parameters a and a are incompatible.
    Type string | number is not assignable to type string
      Type number is not assignable to type string

Это как раз тот самый момент, который и вызвал у меня вопросы.

Ответ нашелся в документации ts и звучит примерно так:

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

Обновлено:
Проблему можно решить, использовав стрелочную функцию:

interface IA {
  setValue: (a: number | string) => void
}

Но, к сожалению, данный подход не универсальный. И если вы не планируете определять метод как свойство, то контролировать корректность имплементации интерфейса описанного через interface или как abscract class нужно самому.

Заключение

Выводов оказалось два.

  1. Обязательно устанавливайте флажок strict. Он подключает дополнительные проверки типов.

  2. При реализации интерфейсов, и работе с объектами корректность соблюдения принципа подстановки Барбары Лисков ложится на плечи разработчиков.

    • использовать стрелочную форму записи метода в типе или интерфейсе

    • следить за импортированными типами

Теги:
Хабы:
Всего голосов 4: ↑3 и ↓1+7
Комментарии27

Публикации

Истории

Работа

Ближайшие события

25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань