in оператор для сужения

Оператор in в TypeScript (и JavaScript) проверяет наличие свойства в объекте: "prop" in obj. TypeScript использует эту проверку как type guard — в ветке true компилятор сужает тип до вариантов, у которых данное свойство точно существует.

Зачем нужно

in позволяет различать объекты по наличию уникального свойства без discriminated union с явным дискриминатором. Это удобно при работе с внешними данными или когда добавление поля kind в тип нецелесообразно.

Где используется

  • Различение типов в union по наличию уникального поля
  • Проверка опциональных свойств перед использованием
  • Narrowing unknown перед обращением к полям
  • Пользовательские type guard функции

Основной контент

Базовое использование

interface Admin {
  role: "admin";
  permissions: string;
}

interface RegularUser {
  role: "user";
  name: string;
}

function describe(account: Admin | RegularUser): string {
  if ("permissions" in account) {
    // account: Admin — у RegularUser нет permissions
    return `Admin with ${account.permissions.length} permissions`;
  }
  // account: RegularUser
  return `User: ${account.name}`;
}

in с опциональными полями

interface WithId {
  id: string;
}

interface WithUuid {
  uuid: string;
}

type Entity = WithId | WithUuid;

function getIdentifier(entity: Entity): string {
  if ("id" in entity) {
    return entity.id;   // entity: WithId
  }
  return entity.uuid;   // entity: WithUuid
}

Narrowing unknown через in

// in требует что левый операнд — string, правый — object
function isError(value: unknown): value is Error {
  return (
    typeof value === "object" &&
    value !== null &&
    "message" in value &&
    "stack" in value
  );
}

function handleError(err: unknown): string {
  if (isError(err)) {
    return err.message; // err: Error
  }
  return String(err);
}

Сравнение с typeof и instanceof

type Cat = { meow:  => void };
type Dog = { bark:  => void };

function makeSound(animal: Cat | Dog): void {
  // typeof не поможет — оба "object"
  // instanceof требует класс

  // in — работает с object literal types
  if ("meow" in animal) {
    animal.meow; // Cat
  } else {
    animal.bark; // Dog
  }
}

in в пользовательском type guard

interface SuccessResponse<T> {
  data: T;
  status: "success";
}

interface ErrorResponse {
  error: string;
  status: "error";
}

type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;

function isSuccess<T>(resp: ApiResponse<T>): resp is SuccessResponse<T> {
  return "data" in resp;
}

async function fetchUser: Promise<ApiResponse<{ name: string }>> {
  return { data: { name: "Alice" }, status: "success" };
}

const resp = await fetchUser;
if (isSuccess(resp)) {
  console.log(resp.data.name); // TypeScript знает: resp.data существует
}

Важное отличие от hasOwnProperty

// in проверяет все свойства (включая прототип)
"toString" in {};        // true (из Object.prototype)
"nonExistent" in {};     // false

// in как type guard работает только с заранее известными свойствами
// для динамических ключей narrowing не происходит
const key = "name";
if (key in obj) {
  // obj[key] всё ещё unknown — TypeScript не сужает по динамическому ключу
}

Частые ошибки

  • Применять in к примитивам"prop" in null выбросит TypeError; всегда проверяйте что значение — объект перед in.
  • Ожидать narrowing по динамическому ключуin как type guard работает только с string literal ("prop" in obj), не с переменной.
  • Путать in с hasOwnPropertyin проверяет цепочку прототипов; это ок для type guard, но учитывайте при работе с прототипами.
  • Использовать in для undefined-свойств — свойство с undefined значением всё равно «в объекте»; in возвращает true для { x: undefined }.

Связанные темы

Ресурсы