javascripttypescriptfunctiontypesparameters

Is there a tsconfig flag or eslint rule to disallow unrelated types as function parameter?


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

enter image description 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'

enter image description here


Solution

  • Preview:


    There 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.

    Playground link to code