简体   繁体   中英

How to use Typescript Function Overloading with a function that has multiple parameters?

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?

TS-Playground

Thank you

Edit

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'

TS-Playground

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

Playground


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
    })
}

See playground

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

Playground

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM