ollama-js icon indicating copy to clipboard operation
ollama-js copied to clipboard

Overloads should be replaced with generics.

Open saul-jb opened this issue 2 months ago • 2 comments

(Just logging a quick issue in case I don't get to a PR) Some of the methods use weird overloads instead of generics and then call generics, this can make it super painful to make code that wraps these methods with the right types without type errors.

The quintessential example is in the generate code: https://github.com/ollama/ollama-js/blob/dfc798866b1b5e690134c6643881a0620e3cedd8/src/browser.ts#L109-L121

The first issue is this makes the stream parameter un-assignable to boolean, meaning this code has a type error:

const generate = (request: GenerateRequest, stream: boolean) => {
  return ollama.generate({ ...request, stream })
}

This is due to ambiguity in which overload signature should be used.

Things get even worse if you want to make stream generic so the right return type can be passed - you have to do different calls for the different generic cases and cast the result to a type conditional.

The overloading works well for simple use cases but generics shouldn't make a difference for the simple uses while making types much easier to work with on more complex ones.

saul-jb avatar Apr 25 '24 23:04 saul-jb

public generate<T extends GenerateRequest>(request: T): Promise<T extends { stream: true } ? AsyncGenerator<GenerateResponse> : GenerateResponse> { ... }

Pixelycia avatar May 04 '24 15:05 Pixelycia

Just been looking at this again and it is not as straight-forward as I had assumed (using conditionals like @Pixelycia mentioned).

You can do it through conditionals but you have to type cast from the specific case to the conditional case every time you return from a method which becomes quite messy.

Another way to get types to behave correctly in most cases is to add a ton of signatures:

  generate(
    request: GenerateRequest & { stream: true },
  ): Promise<AsyncGenerator<GenerateResponse>>
  generate(request: GenerateRequest & { stream: false }): Promise<GenerateResponse>
  generate(request: GenerateRequest & { stream: boolean }): Promise<GenerateResponse | AsyncGenerator<GenerateResponse>>
  generate(request: GenerateRequest): Promise<GenerateResponse>
  async generate(
    request: GenerateRequest & { stream?: boolean },
  ): Promise<GenerateResponse | AsyncGenerator<GenerateResponse>> { ... }
import ollama from './index'

const req = { prompt: '', model: ''}
const stream = Math.random() > 0.5

const a =  ollama.generate({ ...req, stream: true })
const b =  ollama.generate({ ...req, stream: false })
const c = ollama.generate({ ...req, stream: undefined })
const d = ollama.generate(req)
const e = ollama.generate({ ...req, stream })

const test = <S extends { stream?: boolean }>(stream: S) => {
 const f = ollama.generate({ ...req, ...stream })
}

This behaves correctly in cases a-e but fails in case f.

Both adding more signatures or lots of ugly type conditional casts is messy, could we consider splitting the stream cases into separate methods?

If we did something like ollama.stream.generate, we could get types to work in all cases and get rid of that weird return type: Promise<AsyncGenerator<GenerateResponse>>.

Another possibility is to make streaming the default case and export a helper to convert the stream to a single value like:

ollama.generate({...}) // stream: true
await parseStream(ollama.generate({...})) // stream: false

What do you think @BruceMacD?

saul-jb avatar May 14 '24 04:05 saul-jb