Reputation: 2070
I've a function that tries to find a value in an array of numbers. The value can be either an object or a number. If the value is an object, there's a 'key' property that is used to get the number value from the object.
I'm trying to use Function Overloading
to have a single function that can handle both cases.
type ObjectType = {
[key: string]: number
}
type FunctionType = {
<T extends ObjectType>(v: T, list: number[], key: string): T | undefined
(v: number, list: number[]): number | undefined
}
const find: FunctionType = <T extends ObjectType | number,>(v: T, list: number[], key?: string)=>{
const value = typeof v === 'number' ? v : v[key]
return list.find((item)=>{
return item === value
})
}
This creates an error Type undefined cannot be used as an index type
But in this branch, the value of v is an object, so how is key not defined?
Thank you
A solution was proposed by Alex Wayne, but it doesn't properly narrow value to a number.
If instead of calling list.find, we called a custom function that only accepts numbers, typescript would throw an error
const findNumber = (v: number, list: number[])=>{
/* Do stuff with v as a number */
return list.indexOf(v)
}
const find: FunctionType = <T extends ObjectType | number>(v: T, list: number[], key?: keyof T)=>{
const value = (key && typeof v === 'object') ? v[key] : v
return list[findNumber(value, list)]
}
Type 'ObjectType' is not assignable to type 'number'
Upvotes: 3
Views: 1668
Reputation: 33041
First of all, let's make sure that invalid state is unrepresentable. Let's define union types:
type IsObj<T> = {
value: T, key: keyof T
}
type IsNumber = { value: number }
type Params<T> = IsObj<T> | IsNumber
As you might have noticed, I have binded 2nd and 3rd arguments.
Now, we can define our custom typeguard:
const isNumber = <T,>(params: Params<T>): params is IsNumber => typeof params.value === 'number'
Let's put it all together:
type ObjectType = {
[key: string]: number
}
type IsObj<T> = {
value: T, key: keyof T
}
type IsNumber = { value: number }
type Params<T> = IsObj<T> | IsNumber
const isNumber = <T,>(params: Params<T>): params is IsNumber => typeof params.value === 'number'
type FunctionType = {
<T extends ObjectType>(list: number[], params: IsObj<T>): T | undefined
(list: number[], params: IsNumber): number | undefined
}
const find: FunctionType = <T extends ObjectType | number,>(list: number[], params: Params<T>) => {
const value = isNumber(params) ? params.value : params.value[params.key]
return list.find((item) => item === value)
}
const result = find([1, 2, 3], { value: 42}) // ok
const result_ = find([1, 2, 3], { value: { age: 2 }, key: 'age' }) // ok
const result__ = find([1, 2, 3], { value: { age: 2 }, key: 'name' }) // expected error
const result___ = find([1, 2, 3], { value: 42, key: 'name' }) // expected error
Upvotes: 0
Reputation: 186994
The implementation function has no idea what its overloads are. This means that an overload function implementation must still be completely valid if the overload function signatures are removed.
So while it is true that if typeof v === 'number'
then typeof key === 'string'
the compiler doesn't know that.
If you look purely at this function signature:
<T extends ObjectType | number,>(v: T, list: number[], key?: string)=>{
Then you see that find({ someKey: 1 }, [1,2,3])
is a valid invocation. The overloads prevent that invocation, but again this function must be valid without those overloads.
Which means to fix this you'll have to test for the presence of key
before you use it.
const value = (key && typeof v === 'number') ? v : v[key]
However, this gives a new problem:
Type 'undefined' cannot be used as an index type.(2538)
I believe this is because typeof v === 'number'
is not a sufficient refinement. I'm not sure exactly what else it thinks it could be, but if you invert the ternary and instead refine by typeof v === 'object'
typescript is happy.
const value = (key && typeof v === 'object') ? v[key] : v
Lastly, you can probably improve type safety here by using key: keyof T
instead.
const find: FunctionType = <T extends ObjectType | number>(v: T, list: number[], key?: keyof T)=>{
const value = (key && typeof v === 'object') ? v[key] : v
return list.find((item)=>{
return item === value
})
}
Upvotes: 1