Is there some way to create a function that accepts a common interface of two types?
Consider the following example:
interface A {a: number}
type B = {b: string;} & A
type C = {c: string;} & A
function acceptA(a: A) {
return a
}
acceptA({a: 2, c: "type-error"})
Type error:
Argument of type '{ a: number; c: string; }' is not assignable to parameter of type 'A'. Object literal may only specify known properties, and 'c' does not exist in type 'A'.
The acceptA
function doesn't accept an argument of type C
. Is there some way to change acceptA
to allow any type that satisfies the A
interface?
CodePudding user response:
TypeScript's structural type system considers {a: number, c: string}
to be assignable to {a: number}
. So in principle, every value of type C
should be acceptable as a value of type A
. Why, then, does the compiler complain when you pass {a: 2, c: "type-error"}
to a function that accepts a value of type A
?
The answer is that this is a feature known as excess property checking. When you use an object literal like {a: 2, c: "type-error"}
in a context that expects a type like {a: number}
, where the object literal has more properties than the expected type knows about, the compiler warns you because you are probably throwing away information:
The acceptA
function only knows that its input is of type A
; so inside the implementation of acceptA
, you can only really access the a
property. If there is an excess property like c
, you can't reasonably do anything with it inside acceptA
. And anything returned by acceptA
would have a type that also doesn't know about the c
property. So the acceptA
function throws away all excess property information. And because you passed in an object literal, there is no other reference to that object from which you could reasonably do anything with c
outside of acceptA
. So the c
property is essentially useless, both inside and outside of acceptA
... you might as well not even put it in the object literal.
And that's what the warning is about. It's not that the extra property is dangerous or anything; it's perfectly type safe to pass a C
where an A
is expected. It's just that the compiler thinks you're probably doing something you don't mean to be doing.
If you save the object literal to a variable which knows about c
and then pass the variable into acceptA
, there's no error, because you can always grab the c
property from the variable, even if acceptA
can't do anything with it:
const obj = { a: 2, c: "okay" }
// const obj: {a: number, c: string}
acceptA(obj); // <-- no problem now
So before trying to work around this, you should be sure that you really want to. Excess property checking isn't always desirable, but it's there for a reason and you should double check that the reason doesn't apply to you before proceeding.
If you decide that you really don't want to have acceptA()
warn on object literals with excess properties, there are remedies. One remedy is to have acceptA
explicitly accept a value that is both an A
and an "anything-goes" object type like {[k: string]: any}
. A type with string index signature can have any property keys at all:
function acceptA(a: A & { [k: string]: any }) {
a.c // any
return a
}
acceptA({ a: 2, c: "okay" })
That works because the body of acceptA
can now potentially access properties like c
, since all property keys are expected now. There's essentially no such thing as an "excess property" with a string index signature. So even though c
will be invisible outside acceptA
, it is now visible inside.
Or you could make acceptA
generic in the type of the a
argument which is constrained to A
, like this:
function acceptA<T extends A>(a: T) {
return a
}
acceptA({ a: 2, c: "okay" })
That works because the compiler is prepared to keep track of the type of the particular value passed in. In the case above, T
is inferred as {a: number, c: string}
, and so the return type of the function is also {a: number, c: string}
. The body of acceptA
can't do anything with c
(because it doesn't know what T
will be), but because the function is generic, it's still possible in general to be visible outside acceptA
... you could save the result to a new variable and that variable would have a c
property that the compiler might know about.
Note that neither workaround guarantees that type information will not be thrown away; it's just that string index signatures and generic functions make it less likely, and excess property checking no longer applies. There are still lots of completely useless programs you could write that the compiler will not complain about. Excess property checking is just one heuristic that apparently provides a net benefit (although I find that it is such a common source of confusion that sometimes I'm skeptical).