function fetch<T, V>(data: T, callback: (data: T) => V): V;
function fetch<T, K extends keyof T>(data: T, key: K): T[K];
function fetch<T, K extends keyof T>(data: T, key: K | ((data: T) => any)) {
if (typeof key === 'function') {
return key(data);
}
if (key in data) {
return data[key];
}
throw new TypeError('Key getter must be a string or function');
}
const resultA = fetch(1, (a) => a + 1);
const resultB = fetch({a: 'a'}, 'a')
console.log(resultA); // 2
console.log(resultB);// "a"
Clarification: In the previous version of the code, there were some issues that needed to be addressed:
- The return type was already defined in the overloads, so it didn't need to be repeated in the implementation.
- Checking typeof string automatically creates an intersection with the
string
type.
The solution involved using the key in object
syntax, which specifies the type as V[P]
, and the second branch checks for typeof function
.
The specific problem with the intersection of string types is that the original implementation couldn't handle all possible types of keys, such as number | string | symbol
. If a key other than a string is used, the function will throw an exception. For example:
// Example with a symbol key
const symbolKey = Symbol()
const value = fetch({[symbolKey]: 'value'}, symbolKey);
// Example with an array key
const value2 = fetch([1, 2], 1);
// Example with an object containing a number key
const value3 = fetch({1: 'value'}, 1);
All three examples would be correct in terms of type, but would throw an error because the key is not a string. The proposed solution allows all these examples to work correctly by using key in object
to ensure the key is present in the object without requiring a specific key type.
If we specifically want to allow only string keys, the function type definition should reflect that. For example:
function fetch<T, V>(data: T, callback: (data: T) => V): V;
function fetch<T, K extends keyof T & string>(data: T, key: K): T[K];
function fetch<T, K extends keyof T & string>(data: T, key: K | ((data: T) => any)) {
switch (typeof key) {
case 'function':
return key(data);
case 'string':
return data[key];
default: throw new TypeError('Key getter must be a string or function');
}
}
The key difference here is K extends keyof T & string
, which specifies that we accept only keys of type K that are also strings. This approach aligns with the implementation where we check for typeof string
.