Почему утка набирается для классов в TypeScript
Похоже, что в TypeScript это абсолютно нормально (с точки зрения компилятора), чтобы иметь такой код:
class Vehicle {
public run(): void { console.log('Vehicle.run'); }
}
class Task {
public run(): void { console.log('Task.run'); }
}
function runTask(t: Task) {
t.run();
}
runTask(new Task());
runTask(new Vehicle());
Но в то же время я ожидал бы ошибки компиляции, потому что Vehicle
и Task
не имеют ничего общего.
И разумные способы использования могут быть реализованы с помощью явного определения интерфейса:
interface Runnable {
run(): void;
}
class Vehicle implements Runnable {
public run(): void { console.log('Vehicle.run'); }
}
class Task implements Runnable {
public run(): void { console.log('Task.run'); }
}
function runRunnable(r: Runnable) {
r.run();
}
runRunnable(new Task());
runRunnable(new Vehicle());
... или общий родительский объект:
class Entity {
abstract run(): void;
}
class Vehicle extends Entity {
public run(): void { console.log('Vehicle.run'); }
}
class Task extends Entity {
public run(): void { console.log('Task.run'); }
}
function runEntity(e: Entity) {
e.run();
}
runEntity(new Task());
runEntity(new Vehicle());
И да, для JavaScript это абсолютно нормально, чтобы иметь такое поведение, потому что нет классов и никакого компилятора вообще (только синтаксический сахар) и утиная типизация является естественным для языка. Но TypeScript пытается ввести статические проверки, классы, интерфейсы и т.д. Однако утиная типизация для экземпляров классов выглядит довольно запутанной и подверженной ошибкам, на мой взгляд.
Ответы
Ответ 1
Вот как работает структурная типизация. Typescript имеет систему структурного типа, чтобы наилучшим образом подражать тому, как работает Javscript. Поскольку Javascript использует утиную печать, любой объект, который определяет контракт, может использоваться в любой функции. Typescript просто пытается проверять утиную печать во время компиляции, а не во время выполнения.
Однако ваша проблема будет проявляться только для тривиальных классов, как только вы добавляете рядовых, классы становятся несовместимыми, даже если они имеют одинаковую структуру:
class Vehicle {
private x: string;
public run(): void { console.log('Vehicle.run'); }
}
class Task {
private x: string;
public run(): void { console.log('Task.run'); }
}
function runTask(t: Task) {
t.run();
}
runTask(new Task());
runTask(new Vehicle()); // Will be a compile time error
Это поведение также позволяет вам явно не реализовывать интерфейсы, например, вы можете определить интерфейс для параметра inline, и любой класс, который удовлетворяет контракту, будет совместим, даже если они явно не реализуют какой-либо интерфейс:
function runTask(t: { run(): void }) {
t.run();
}
runTask(new Task());
runTask(new Vehicle());
В личном случае, исходя из С#, сначала это казалось безумным, но когда дело доходит до расширяемости, этот способ проверки типов позволяет значительно повысить гибкость, как только вы привыкнете к нему, вы увидите преимущества.
Ответ 2
Теперь с помощью TypeScript можно создавать номинальные типы, которые позволяют различать типы по контексту. Пожалуйста, рассмотрите следующий вопрос:
Атомная дискриминация типов (номинальные атомные типы) в TypeScript
С этим пример:
export type Kilos<T> = T & { readonly discriminator: unique symbol };
export type Pounds<T> = T & { readonly discriminator: unique symbol };
export interface MetricWeight {
value: Kilos<number>
}
export interface ImperialWeight {
value: Pounds<number>
}
const wm: MetricWeight = { value: 0 as Kilos<number> }
const wi: ImperialWeight = { value: 0 as Pounds<number> }
wm.value = wi.value; // Gives compiler error
wi.value = wi.value * 2; // Gives compiler error
wm.value = wi.value * 2; // Gives compiler error
const we: MetricWeight = { value: 0 } // Gives compiler error