Is there a tsconfig flag or eslint rule to disallow this ?
interface Order {
title?: string;
orderName?: string;
}
interface Product {
title?: string;
productId?: string;
}
var product: Product = {}
function filterOrder(order: Order) {
if (order.orderName == null) return true;
}
filterOrder(product) // i want an error here
The problem is that passing a Product in filterOrder is a mistake by the developer. filterOrder should only filter Orders, not Products.
I know that based on the type definition, Product is a valid Order.
But in practice, this is just a coincidence that they have the same field name. I don't want functions that should take Order
also take Product
The function uses orderName
and products don't have that.
Strangely
If I comment Product.title, I get the an error Type 'Product' has no properties in common with type 'Order'
Preview:
never
to fix thatThere is no such setting and if there were I imagine it would give many, many false positives across most code bases. TypeScript's type system is fundamentally structural and not nominal. The shape of the types is what TypeScript cares about, not their names or declaration sites. There is a (practically ancient) feature request at microsoft/TypeScript#202 to support some nominal typing, so you could maybe say that only a value explicitlyannotated as Order
is allowed to be assigned to Order
. But this is not part of the language.
If a Product
is a valid Order
then no matter what you do, there will be some way to assign a Product
to a place that expects an Order
.
Therefore, if it's very important that these particular two types not be confused with each other, you should define them to be seen as incompatible. I'd be inclined to give each one an optional property of the impossible never
type corresponding to the other interface, like:
interface Order {
title?: string;
orderName?: string;
productId?: never;
}
interface Product {
title?: string;
productId?: string;
orderName?: never;
}
This is as close as you can get to saying that an Order
should not have a productId
property, and that a Product
should not have an orderName
property. If you do this, you get your desired behavior:
var order: Order = {}
filterOrder(order); // okay
var product: Product = {}
filterOrder(product); // error
Note that this depends on you knowing in advance which types people are likely to conflate. You can't say "all unexpected properties are errors". That would require so-called exact types, as requested in microsoft/TypeScript#12936, and TypeScript doesn't have those. For better or worse, TypeScript allows excess properties in the type system. Even though there are some cases where it will warn you (e.g., there are excess property checks on object literals and there is weak type detection), there are always ways around these.
You can sort of simulate exact types by using generics:
type Exact<T extends U, U> =
T & { [K in keyof T]: K extends keyof U ? U[K] : never }
function filterOrder<T extends Order>(order: Exact<T, Order>) {
if (order.orderName == null) return true;
return;
}
var order: Order = {}
filterOrder(order); // okay
var product: Product = {}
filterOrder(product); // error!
const thing = { title: "abc", orderName: "def", randomThing: "oops" }
filterOrder(thing); // error!
This works by inferring your generic type argument T
to be the input type, and then mapping extra properties to never
. It is as close as I know how to get to exact types in TypeScript. It's not perfect, because the type system fundamentally doesn't care about it. An indirect assignment will evade detection:
order = product; // allowed!
filterOrder(order); // allowed!
If you want order = product
to be an error, then you're back to the first approach of making them truly incompatible.
If you want to simulate nominal typing, the best you can do is to give your type a required property which is incredibly unlikely to occur elsewhere, at least not accidentally. You are effectively branding your type. Maybe like this:
interface Order {
type: "Order";
title?: string;
orderName?: string;
}
function filterOrder(order: Order) {
if (order.orderName == null) return true;
return;
}
Here we've given Order
a required type
property of the literal type "Order"
. That will prevent bad assignments:
const product: Product = {}
filterOrder(product); // error
But you are now required to actually provide that property.
const order: Order = { type: "Order" }; // okay
filterOrder(order);
If someone adds type: "Order"
to their Product
instances then that's probably no longer a mistake. I mean, you might want to prevent that, but it's unlikely to be an accident.