Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does TypeScript not throw an error when the function argument type is mismatched?

Here's a very basic example to demonstrate what I mean:

type Payload = {
    id: number;
}

type GreatPayload = {
    id: number;
    surprise: 4;
}

type Action = (payload: Payload) => void;

const action: Action = payload => null;

const payload: GreatPayload = {
    id: 1,
    surprise: 4,
};

action({ id: 1, surprise: 4 }); // <== as expected, this errors out because `surprise` is not present in `Payload`

action(payload); // <== my question is, why does this not throw an error?

(And the TypeScript playground link for an editable example.)

Why does action(payload) not throw an error when the type of payload that's passed in (GreatPayload) is clearly a mismatch for the function argument type Payload?

like image 810
Baggio Wong Avatar asked May 16 '20 18:05

Baggio Wong


People also ask

How do you declare a function that throws an error in TypeScript?

To declare a function that throws an error, set its return type to never . The never type is used for functions that never return a value, in other words functions that throw an exception or terminate execution of the program.

What happens when you throw an error in TypeScript?

The throw statement throws a user-defined exception. Execution of the current function will stop (the statements after throw won't be executed), and control will be passed to the first catch block in the call stack. If no catch block exists among caller functions, the program will terminate.

Is it bad to use any type in TypeScript?

❌ Don't use any as a type unless you are in the process of migrating a JavaScript project to TypeScript. The compiler effectively treats any as “please turn off type checking for this thing”.

What is type error in TypeScript?

The TypeError object represents an error when an operation could not be performed, typically (but not exclusively) when a value is not of the expected type. A TypeError may be thrown when: an operand or argument passed to a function is incompatible with the type expected by that operator or function; or.


1 Answers

Object types in TypeScript are open/extendible, not closed/exact. That means it's acceptable for an object of type X to contain more properties than the definition of X mentions. You can think of object type definitions as describing the known properties of the type, while having no implications about possible unknown properties.

This openness is important because it allows interface extension and class inheritance. Your type definitions are nearly identical to

interface Payload {
    id: number;
}

interface GreatPayload extends Payload {
    surprise: 4;
}

And here you can see that GreatPayload is a special type of Payload. It has an extra property, but it's still a Payload. Same thing with class inheritance:

class Foo {
    a = "foo";
}
class Bar extends Foo {
    b = "bar";
}

A Bar instance is a Foo:

const f: Foo = new Bar(); // okay

The only place where the TypeScript compiler treats object types as if they were exact is when you create a brand new object literal and assign it to a type. This is documented in the TypeScript Handbook as "Excess Property Checks"... and you can also look at microsoft/TypeScript#3755, the GitHub issue that discusses the need for this behavior; misspelling optional properties would be completely uncaught errors without some sort of key checking like this. But it's not a full implementation of exact types.


So when you call this:

action({ id: 1, surprise: 4 });  // error

you are passing in a fresh object literal that contains an unexpected surprise property, and the compiler warns via excess property checks. But when you call this:

action(payload);  // okay

you are passing in the variable payload, which is not an object literal itself, and the object literal you assigned to payload is no longer "fresh". So no excess property checks happen and you get no warning.


If you really want to see exact types implemented so that you could easily ask for Exact<Payload>, you might want to go to microsoft/TypeScript#12936 and give it a 👍, and possibly even describe your use case if it's particularly compelling.

But given that the current behavior is probably not going anywhere for a while, your time might be better spent trying to work with open types instead of against them. Consider writing your code so that it doesn't mind if an object has more properties than specified in the type declaration. If you're just indexing into the object with known keys, you'll be fine. If you're iterating through object properties, don't use Object.keys() or for..in loops if your code would explode on unexpected properties. Instead, think about iterating through known keys from a hardcoded array (see this answer for one way to do this). The idea is to make your code immune to unknown extra properties so that you don't care if someone gives you a GreatPayload when you're expecting just a Payload.

Okay, hope that helps; good luck!

Playground link to code

like image 96
jcalz Avatar answered Oct 29 '22 17:10

jcalz