krisk / Fuse

Lightweight fuzzy-search, in JavaScript
https://fusejs.io/
Apache License 2.0
18.1k stars 766 forks source link

Fuse.search doesn't return only T[] #265

Closed Dante-101 closed 4 years ago

Dante-101 commented 5 years ago

Current TS definition says,

search(pattern: string): T[]

But with includeScore, the search returns: search(pattern: string): T[] | { item: T, score: number }[];

For now, I am ignoring the return type with // @ts-ignore.

mina-skunk commented 5 years ago

There is more:

with id search(pattern: string): string[];

with includeMatches search(pattern: string): { item: T; matches: any; }[];

with id and includeMatches search(pattern: string): { item: string; matches: any; }[];

its really ugly but modifying the definition file like this seems to work

export = Fuse;
export as namespace Fuse;

declare class Fuse<T, O extends Fuse.FuseOptions<T> = Fuse.FuseOptions<T>> {
  constructor(list: ReadonlyArray<T>, options?: O)
  search(pattern: string): O extends { id: keyof T } ?
    O extends ({ includeMatches: true; } | { includeScore: true; }) ? Fuse.FuseResult<string>[] : string[] :
    O extends ({ includeMatches: true; } | { includeScore: true; }) ? Fuse.FuseResult<T>[] : T[];

  setCollection(list: ReadonlyArray<T>): ReadonlyArray<T>;
}

declare namespace Fuse {
  export interface FuseResult<T> {
    item: T,
    matches?: any;
    score?: number;
  }
  export interface FuseOptions<T> {
    id?: keyof T;
    caseSensitive?: boolean;
    includeMatches?: boolean;
    includeScore?: boolean;
    shouldSort?: boolean;
    sortFn?: (a: { score: number }, b: { score: number }) => number;
    getFn?: (obj: any, path: string) => any;
    keys?: (keyof T)[] | { name: keyof T; weight: number }[];
    verbose?: boolean;
    tokenize?: boolean;
    tokenSeparator?: RegExp;
    matchAllTokens?: boolean;
    location?: number;
    distance?: number;
    threshold?: number;
    maxPatternLength?: number;
    minMatchCharLength?: number;
    findAllMatches?: boolean;
  }
}
dgreene1 commented 5 years ago

@gatimus hats off to you. Your solution appears to be the correct one. I'd love it if you could PR the repo with that solution. (note: I'm not the maintainer. I just think you created some awesome types that would be really helpful for many people 🥇 👍 )

zhouzi commented 4 years ago

The type definition seems to have been updated with @william-lohan's suggestions but it's unclear how it's supposed to work. Could someone post an example?

Here's what I came up with:

interface User {
  email: string;
}

const FUSE_OPTIONS: Fuse.FuseOptions<User> & { includeScore: true } = {
  includeScore: true,
  keys: ['email']
};

new Fuse<User, typeof FUSE_OPTIONS>([...]).search('query')

The & { includeScore: true } part is weird but without that it considers includeScore to be as Fuse.FuseOptions defines it: boolean | undefined. It doesn't seem to satisfy the O extends ({ includeMatches: true; } | { includeScore: true; }) as it doesn't return a Fuse.FuseResult<T> but a T.

mina-skunk commented 4 years ago

@Zhouzi it errors on the side of optional properties being undefined (ie includeScore in optional so according to the conditional return it is undefined unless explicitly shown as defined ex your & { includeScore: true }). It works better if you let TypeScript infer the type from the object literal. See res1 below.

import * as Fuse from "fuse.js";

interface User {
  email: string;
}

const src: User[] = [];

const res1: Fuse.FuseResult<User>[] = new Fuse(src, {
  includeScore: true,
  keys: ["email"]
}).search("query");

const res2: User[] = new Fuse(src, {
  keys: ["email"]
}).search("query");

const emailKey: keyof User = "email";

const res3: Fuse.FuseResult<string>[] = new Fuse(src, {
  id: emailKey,
  includeScore: true,
  keys: ["email"]
}).search("query");

const res4: string[] = new Fuse(src, {
  id: emailKey,
  keys: ["email"]
}).search("query");

I could make some improvement on the keyof User"weirdness" in the last 2 examples but haven't came up with anything yet.

zhouzi commented 4 years ago

@william-lohan thank you! 👍 Following your example, I got it working like so:

interface User {
  email: string;
}
const users: User[] = [
  {
    email: 'jane@doe.com'
  },
  {
    email: 'john@doe.com'
  }
];
const matchingUsers: User[] =
  new Fuse(users, {
    includeScore: true,
    shouldSort: false,
    keys: ['email']
  })
    .search('jane')
    .map(result => result.item);

There are two important limitations: Fuse.FuseOptions cannot be used and the options object has to be inlined. For example, the following doesn't work:

interface User {
  email: string;
}
const users: User[] = [
  {
    email: 'jane@doe.com'
  },
  {
    email: 'john@doe.com'
  }
];
const FUSE_OPTIONS = {
  includeScore: true,
  shouldSort: false,
  keys: ['email']
};
const matchingUsers: User[] =
  new Fuse(users, FUSE_OPTIONS)
    .search('jane')
    .map(result => result.item);

Anyway, I believe the initial issue has been fixed and can be closed.

github-actions[bot] commented 4 years ago

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days