Как предотвратить непреднамеренную совместимость типов в TypeScript

#typescript

Вопрос:

Допустим, у нас есть интерфейс Animal

 interface Animal {
  amountOfLegs: number;
}
 

А затем два разных класса реализуют этот интерфейс, Dog и Snake :

 class Dog implements Animal {
  constructor(public amountOfLegs: number) {
  }
}

class Snake implements Animal {
  amountOfLegs: number;

  constructor() {
    this.amountOfLegs = 0;
  }
}
 

Затем мы хотим создать функцию, специфичную для Змеи:

 function functionSpecificForSnake(snake: Snake): any {
  //Do something with the snake object...
}
 

Но когда функция вызывается с помощью объекта Dog, TS не жалуется на неправильные типы:

 functionSpecificForSnake(new Dog(4)); // No compiler error, but possible runtime error?
 

Ссылка на игровую площадку

Итак, мой вопрос: могу ли я предотвратить такое поведение? Я знаю, что это называется совместимостью типов, но я не вижу способа предотвратить это. Использование "strictFunctionTypes": true опции в моем tsconfig.json, похоже, ничего не дает.

Комментарии:

1. Я рекомендую добавить ссылку на игровую площадку для машинописи к вашему вопросу. typescriptlang.org/play/index.html

2. @sunknudsen готово

Ответ №1:

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

Для этого вы можете добавить свойство с именем что-то вроде __brand , которое не противоречит какому-либо реальному свойству , которое может иметь тип, и свойство может иметь отдельный тип для каждого класса; это может быть просто сам класс или имя класса в виде строкового литерала. Чтобы избежать каких-либо затрат во время выполнения, вы можете объявить свойство без его инициализации, поэтому свойство на самом деле не существует, но Typescript думает, что оно существует. Чтобы отключить ошибку для неинициализированного свойства, вы можете сделать это свойство необязательным (или использовать @ts-ignore ).

 class Dog implements Animal {
  private readonly __brand?: Dog;

  constructor(public amountOfLegs: number) {}
}

class Snake implements Animal {
  private readonly __brand?: Snake;

  amountOfLegs: number;
  constructor() {
    this.amountOfLegs = 0;
  }
}
 

Затем, если вы попытаетесь использовать a Dog там, где Snake ожидается a, вы получите ошибку типа, потому __brand что свойство имеет неправильный тип.

Ссылка на игровую площадку

Ответ №2:

Я считаю, что TypeScript проверяет типы свойств, поэтому ваша проблема заключается в том, что они оба Dog Snake имеют одинаковые свойства и типы… так что они идентичны.

См.TypeScript Playground.

 interface Animal {
  amountOfLegs: number;
}
class Dog implements Animal {
  constructor(public amountOfLegs: number) {
  }
}
class Snake implements Animal {
  public amountOfLegs: number;
  public venomous: boolean;
  constructor() {
    this.amountOfLegs = 0;
    this.venomous = true;
  }
}
function functionSpecificForSnake(snake: Snake): void {
  //Do something with the snake object...
}
functionSpecificForSnake(new Dog(4));
 

Ответ №3:

Как бы то ни было, нет, ты не можешь. В ссылке, на которую вы ссылались: «Чтобы проверить, может ли y быть присвоен x, компилятор проверяет каждое свойство x, чтобы найти соответствующее совместимое свойство в y». Таким образом, компилятор игнорирует бит «реализует интерфейсы» классов и просто смотрит на форму каждого класса.

Змеи и собаки имеют одинаковую форму, поэтому их можно отнести друг к другу.

Вы могли бы попробовать добавить дискриминатор в каждый класс, что-то вроде:

 interface Animal {
    disc: string
    amountOfLegs: number;
}

class Dog implements Animal {
    disc = 'Dog' as const
    constructor(public amountOfLegs: number) {
    }
}

class Snake implements Animal {
    disc = 'Snake' as const
    amountOfLegs: number;

    constructor() {
        this.amountOfLegs = 0;
    }
}

function functionSpecificForSnake(snake: Snake): any {
    //Do something with the snake object...
}

functionSpecificForSnake(new Dog(4))
 

Теперь это приводит к ошибке

Аргумент типа «Собака» не может быть присвоен параметру типа «Змея». Типы свойств «диск» несовместимы. Тип «Собака» не может быть присвоен типу»Змея». (2345)

Ответ №4:

Уже есть несколько хороших объяснений, но я также рекомендую посмотреть здесь. Это помогло мне глубже понять проблему.