How to return different types based on parameters in a React.useCallback

Viewed 269

I want to return an object when I have a certain prop in the parameter of my function, and a string when not.

I can't use functions overloads because it is used in a type that is shared with React components, in React.useCallback.

I tried:

type FileType = {
    name: string
    content: string
}

// This is a type that I use in many part of my code. I would like the result of onSave to depend on the props it receive
type Props = {
    onSave: <T extends FileType | undefined>(data: {
        id: string
        file: T
    }) => T extends FileType ? FileType : string
}

const isFile = (o: unknown): o is FileType => {
    return o !== undefined
}


// Here you can read the error
const insert: Props['onSave'] = prop => isFile(prop.file) ? prop.file : prop.id)

// Here is how the function is written in my code. But React doesn't exist in this playground
// const insert = React.useCallback<Props['onSave']>(prop => isFile(prop.file) ? prop.file : prop.id, [])

the error:

Type 'string | (T & FileType)' is not assignable to type 'T extends FileType ? FileType : string'.
  Type 'string' is not assignable to type 'T extends FileType ? FileType : string'.

Playground Link: Provided

1 Answers

Here is what you can do: playground

Basically, first you need to define the parametric result using generics:

type FileType = {
    name: string
    content: string
}

type Id = { id: string }

type WithFile = Id & { file: FileType }

type OnSaveResult<T extends WithFile | Id> = T extends WithFile ? FileType : string

Now you can define your general onSave function type:

type Props = {
    onSave: <T extends WithFile | Id = Id>(data: T) => OnSaveResult<T>
}

Now, the trick is to cast the result of your function with the parametric result. To avoid hiding some type issues, it can be interesting to manually cast the result of each result returned based on what we know it should look like.

// Define your function using generics
const onSave = <T extends WithFile | Id>(prop: T) => (hasFile(prop)
    // Might be interesting to manually do the cast here to ensure that each type is valid. Otherwise, prop.file.content, for instance, would not raise an error
    ? prop.file as OnSaveResult<WithFile>
    // Same here, it might be worth manually enforcing the type checking
    : prop.id as OnSaveResult<Id>
// We need to manyally cast to OnSaveResult<T> for the result type to adapt to the generic on input
) as OnSaveResult<T>

Your function is now inferring the types correctly based on props

const resultsToString = onSave({ id: 'test' }) // string
const resultsToFile = onSave({ id: 'test', file: { name: 'test', content: 'test'}}) // FileType

In your useCallback, you can use these syntaxes based on the case:

// Only in the components that might need files, you can use this to declare your function
const insert = React.useCallback<Props['onSave']>(<T extends WithFile | Id>(prop: T) => (hasFile(prop) ? prop.file : prop.id) as OnSaveResult<T>, [])

// In more basic components, you might use a simpler version
const insert2 = React.useCallback<Props['onSave']>(prop => prop.id, [])
Related