If you aim to achieve compiler-verified type safety, both in the caller's context and within the implementation itself, consider restructuring your code like so:
function createAccessor<R extends [array?: unknown[]], U>(
indexfn: (index: number, ...r: R) => U,
...r: R
) {
return {
valueAt(index: number) {
return indexfn(index, ...r);
},
};
}
Using a rest parameter of an optional tuple type for array
instead of an optional parameter will enable the compiler to understand that array
might be absent only if indexfn
doesn't require any additional parameters after index
. In your current version, both array
and r
are optional parameters, potentially leading to independent presence or absence.
By utilizing spread syntax with rest parameters for the final argument, we ensure clarity for the compiler on what is transpiring (replacing it with indexfn(index, r[0])
would prompt a compiler error due to lack of equivalence visibility).
Thus, this approach aligns with your intended behavior:
const x = createAccessor(() => 1); // valid
const y = createAccessor((index) => index); // valid
const z = createAccessor((index, v) => v[index], ["a", "b", "c"]); // valid
const w = createAccessor((index, v) => v[index]); // invalid!
const x100 = x.valueAt(100); // yields a number
const y10 = y.valueAt(10); // yields a number
const z1 = z.valueAt(1); // yields a string
Alternatively, if maintaining your current implementation (perhaps due to performance considerations involving the spread operator – though caution against premature optimization), but ensuring satisfaction for callers, one could implement overloads to differentiate between two types of function calls based on unique call signatures:
interface ValueAt<U> {
valueAt(index: number): U;
}
// distinct call signatures
function createAccessor<U>(indexfn: (index: number) => U): ValueAt<U>;
function createAccessor<T extends unknown[], U>(
indexfn: (index: number, arr: T) => U, array: T): ValueAt<U>;
// actual implementation
function createAccessor(indexfn: (index: number, arr?: any[]) => any, array?: any[]
) {
return {
valueAt(index: number) {
return indexfn(index, array);
},
};
}
This approach mirrors the caller's perspective closely; either invoke the function with one argument demanding a single-parameter indexfn
, or with two arguments necessitating a two-parameter indexfn
. Thus, indexfn
and array
aren't independently optional:
const x = createAccessor(() => 1); // acceptable
const y = createAccessor((index) => index); // acceptable
const z = createAccessor((index, v) => v[index], ["a", "b", "c"]); // acceptable
const w = createAccessor((index, v) => v[index]); // error!
const x100 = x.valueAt(100); // yields a number
const y10 = y.valueAt(10); // yields a number
const z1 = z.valueAt(1); // yields a string
The implementation undergoes loose verification, hence altering return indexfn(index, array)
to return "oopsiedoodle"
wouldn't raise compiler complaints. Awareness of potential consequences is key in such scenarios.
Link to playground for code testing