I am getting:
Overload signature is not compatible with function implementation.ts(2394)
On:
/** Iterate through an Array. */
export default function eachr<Value>(
subject: Array<Value>,
callback: IteratorCallback<typeof subject, number, Value>
): typeof subject
Of this entire snippet:
export interface IteratorCallback<Subject, Key, Value> {
(this: Subject, value: Value, key: Key, subject: Subject): void | boolean
}
/** Iterate through an Array. */
export default function eachr<Value>(
subject: Array<Value>,
callback: IteratorCallback<typeof subject, number, Value>
): typeof subject
/** Iterate through an Object. */
export default function eachr<RecordKey extends keyof any, Value>(
subject: Record<RecordKey, Value>,
callback: IteratorCallback<typeof subject, RecordKey, Value>
): typeof subject
/** Iterate through the subject. */
export default function eachr<RecordKey extends keyof any, Value>(
input: Array<Value> | Record<RecordKey, Value>,
callback: IteratorCallback<typeof input, RecordKey | number, Value>
): typeof input {
if (Array.isArray(input)) {
// Array
const subject = input as Array<Value>
for (let key = 0; key < subject.length; ++key) {
const value = subject[key]
if (callback.call(subject, value, key, subject) === false) {
break
}
}
} else {
// Object
const subject = input as Record<RecordKey, Value>
for (const key in subject) {
if (subject.hasOwnProperty(key)) {
const value = subject[key]
if (callback.call(subject, value, key, subject) === false) {
break
}
}
}
}
// Return
return input
}
I could make it work by changing it to:
/** Iterate through an Array. */
export default function eachr<Subject extends Array<Value>, Value>(
subject: Subject & Array<Value>,
callback: IteratorCallback<typeof subject, number, Value>
): typeof subject
However, I don't understand why that fixed it. What was the problem, and why did that change make the problem go away?
What is even more surprising to me, is that if I apply that same change to a pure object iterator function, it causes it to fail:
/** Iterate through an Object. */
export default function eachrObject<
Subject extends Record<RecordKey, Value>,
RecordKey extends keyof any,
Value
>(
subject: Subject & Record<RecordKey, Value>,
callback: IteratorCallback<typeof subject, RecordKey, Value>
): typeof subject {
for (const key in subject) {
if (subject.hasOwnProperty(key)) {
const value = subject[key]
// above fails with: Element implicitly has an 'any' type because type 'Record<RecordKey, Value>' has no index signature.ts(7017)
// below fails with: Argument of type 'string' is not assignable to parameter of type 'RecordKey'.ts(2345)
if (callback.call(subject, value, key, subject) === false) {
break
}
}
}
return subject
}
Whereas this works:
/** Iterate through an Object. */
export default function eachrObject<RecordKey extends keyof any, Value>(
subject: Record<RecordKey, Value>,
callback: IteratorCallback<typeof subject, RecordKey, Value>
): typeof subject {
for (const key in subject) {
if (subject.hasOwnProperty(key)) {
const value = subject[key]
if (callback.call(subject, value, key, subject) === false) {
break
}
}
}
return subject
}
Yet both forms work fine for the Array iterator:
/** Iterate through an Array. */
export default function eachrArray<Subject extends Array<Value>, Value>(
subject: Subject & Array<Value>,
callback: IteratorCallback<typeof subject, number, Value>
): typeof subject {
for (let key = 0; key < subject.length; ++key) {
const value = subject[key]
if (callback.call(subject, value, key, subject) === false) {
break
}
}
return subject
}
/** Iterate through an Array. */
export default function eachrArray<Value>(
subject: Array<Value>,
callback: IteratorCallback<typeof subject, number, Value>
): typeof subject {
for (let key = 0; key < subject.length; ++key) {
const value = subject[key]
if (callback.call(subject, value, key, subject) === false) {
break
}
}
return subject
}
So how come the change to Subject extends Array<Value>
was necessary for overloading compatibility for the areay iterator, yet Subject extends Record<RecordKey, Value>
breaks the object iterator?
Sorry for the shear amount of code here, this was the minimum use case I could bring it down to, that contained all the considerations at play.
Problem
This has to do with how union types work. The problem originates from the last (accumulative) overload:
callback: IteratorCallback<typeof input, RecordKey | number, Value>
Because input
here is of type Array<Value> | Record<RecordKey, Value>
, the definition for callback
constructed this way allows 4 possible combinations to exist:
IteratorCallback<Array<Value>, RecordKey, Value>
IteratorCallback<Array<Value>, number, Value>
IteratorCallback<Record<RecordKey, Value>, RecordKey, Value>
IteratorCallback<Record<RecordKey, Value>, number, Value>
but only 2 of them are valid according to your preceding overload definition
Solution
This can be fixed by saying callback
will be either one of these two types:
callback: IteratorCallback<Array<Value>, number, Value> | IteratorCallback<Record<RecordKey, Value>, RecordKey, Value>
This takes care of the Overload signature is not compatible with function implementation error.
However, another issue has been uncovered: TypeScript doesn't make the connection between the type of provided input
and the callback
that goes with it. Because the last overload still uses union types — two for input
and two for callback
— there are 4 scenarios TypeScript thinks can happen. It seems the most popular workaround for this problem is just using a type assertion.
Honestly, this is a lot to wade through, and I don't think I can answer exactly why you've gotten things to work. In my opinion, your overload signatures should both fail. Let's look at a super simple overload/implementation example:
function foo(x: string): void; // narrower, okay
function foo(x: string | number | boolean): void; // wider, error
function foo(x: string | number): void {} // impl
Notice how the second overload signature gives the error that it is not compatible with the implementation signature. That's because the overload's x
is a wider type than the implementation's x
. And overloads require narrower types.
Also note how in general (since --strictFunctionTypes
was introduced in TypeScript 2.6) function types are contravariant in their parameter types. That results in the following behavior:
type StringAccepter = (x: string) => void;
const helloAccepter: StringAccepter = (x: "hello") => {}; // error
const stringOrNumberAccepter: StringAccepter = (x: string | number) => {}; // okay
helloAccepter
is not a valid StringAccepter
because "hello"
is narrower than string
, while stringOrNumberAccepter
is a valid StringAccepter
because string | number
is wider than string
. And thus function parameters becoming wider make their functions narrower and vice versa:
function bar(cb: (x: "hello")=>void): void; // error, cb is too wide because x is too narrow
function bar(cb: (x: string | number)=>void): void; // okay, cb is narrower because x is wider
function bar(cb: StringAccepter): void {} // impl
So I'd expect both of your overloads to fail, since the implementation signature's callback
type (IteratorCallback<typeof input, RecordKey | number, Value>
) is actually narrower than either of your call signature's callback
types.
At this point, instead of trying to slog through your possible solution involving an extra Subject
type parameter and understanding why some things work and some things don't (which makes my brain hurt... maybe there's a compiler bug? maybe not? who knows), I'll instead go with the solution I'd suggest... make the implementation signature truly wide enough to support both call signatures:
/** Iterate through an Array. */
export function eachr<Value>(
subject: Array<Value>,
callback: IteratorCallback<typeof subject, number, Value>
): typeof subject
/** Iterate through an Object. */
export function eachr<RecordKey extends keyof any, Value>(
subject: Record<RecordKey, Value>,
callback: IteratorCallback<typeof subject, RecordKey, Value>
): typeof subject
/** Iterate through the subject. */
export function eachr<RecordKey extends keyof any, Value>(
input: Array<Value> | Record<RecordKey, Value>,
// here is the change
callback: IteratorCallback<Array<Value>, number, Value> |
IteratorCallback<Record<RecordKey, Value>, RecordKey, Value>
): typeof input {
if (Array.isArray(input)) {
// Array
const subject = input as Array<Value>
// a new assertion:
const cb = callback as IteratorCallback<Array<Value>, number, Value>;
for (let key = 0; key < subject.length; ++key) {
const value = subject[key]
if (cb.call(subject, value, key, subject) === false) {
break
}
}
} else {
// Object
const subject = input as Record<RecordKey, Value>
// a new assertion:
const cb = callback as IteratorCallback<Record<RecordKey, Value>, RecordKey, Value>;
for (const key in subject) {
if (subject.hasOwnProperty(key)) {
const value = subject[key]
if (cb.call(subject, value, key, subject) === false) {
break
}
}
}
}
// Return
return input
}
The difference is the callback
parameter on the implementation signature is a true union of the analogous types of the callback
parameter on each call signature. Additionally the implementation itself needs to do a narrowing assertion for callback
to cb
in much the same way as the assertion you're already doing for input
to subject
.
Now the compiler should be happy. Hope that helps; good luck!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With