Почему проверка типа через instanceof не работает с интерфейсами в TypeScript
Что проверяют
Разделяете ли вы систему типов TypeScript, которая живёт только на этапе компиляции, и рантайм JavaScript. Это отделяет тех, кто понимает, что происходит со сборкой, от тех, кто пишет типы по образцу.
Как отвечать
instanceof это оператор JavaScript: он идёт по цепочке прототипов объекта и ищет в ней Ctor.prototype, поэтому справа обязан стоять существующий в рантайме конструктор. Интерфейс TypeScript существует только для проверки типов, в собранный код от него не попадает ничего, и implements тоже не оставляет метки на объекте. Проверять нечего.
Чем заменяют:
- пользовательский type guard с предикатом
obj is Animalи проверкой полей; - размеченное объединение с полем вроде
kindи обычнымswitch, самый дешёвый и надёжный вариант; - абстрактный класс вместо интерфейса, если
instanceofдействительно нужен; - схема-валидатор на границе приложения, когда данные пришли из сети.
interface Animal { name: string }
function isAnimal(v: unknown): v is Animal {
return typeof v === "object" && v !== null && "name" in v;
}Полезно добавить, что предикат это обещание компилятору, а не проверка. Содержимое такой функции никто не верифицирует, и неверный guard просто отключает типизацию в этой ветке.
Где спотыкаются
Пишут obj instanceof Animal и получают ошибку про то, что имя ссылается на тип, а используется как значение. Дальше иногда добавляют объявление переменной с тем же именем, ошибка исчезает, а падение переезжает в рантайм.
Пишут guard как return "name" in obj с аргументом типа any. На null и на примитиве оператор in бросает TypeError, а any заранее выключил проверки, которые это поймали бы. Аргумент надо брать как unknown и сначала убеждаться, что это объект.
Забывают, что instanceof ломается и на классах. Объект из другого окна или из второй копии библиотеки в бандле имеет другой прототип, и проверка вернёт false при полностью совпадающей форме. Поэтому в библиотечном коде тип определяют по полю-маркеру.