generics don't seem to be taken fully into account in typescript function

Viewed 44

I have the following setup:

export const doQuery = <DocType>({ query }: { query: Query<DocType> }) => {
  return query.doTheQuery()
}

When I go to use it:

export const CollectionCount = ({ docType }: { docType: DocTypeKeys }) => {
  const query = createQuery(docType)
  
  return doQuery({ query })
}

DocType can be A, B or C. However I'm getting an error on query within doQuery:

Type 'Query<A> | Query<B> | Query<C>' is not assignable to type 'Query<A>'

I'm confused how typescript is thinking that query can only be Query<A> in this case

1 Answers

You have several options here, each with its own benefits and drawbacks. First:

const doQuery = <D extends A | B | C, T extends Query<D>>({ query }: { query: T }): D => {
  return query.doTheQuery()
}

Good thing about it - type safety inside doQuery implementation. Bad thing, it has its type glitches when you use it.

function foo(q: Query<A>): A {
    const a = doQuery({query: q}); // Bad: "a" is typed to A | B | C
    const a1: A = a; // Error because A | B | C can not be assigned to A

    const b: B = doQuery({query: q}) // Good: it errors in incompatible query

    const c: A = doQuery({query: q}) // Good: it assignes without typecasting to the correct type
    const c1: A = c // No error
}

Other option is to type it like that

const doQuery = <T extends Query<any>>({ query }: { query: T }): T extends Query<infer V> ? V : never => {
  return query.doTheQuery()
}

Good: it has the best declaration in terms of usage (like option 3). It always has correct return type. Bad: you should be careful inside the implementation.

const doQuery = <T extends Query<any>>({ query }: { query: T }): T extends Query<infer V> ? V : never => {
  const result = query.doTheQuery()

  const a = 6 + result; // No error, because "result" on this step inferred as "any"

  return result;
}

And finally, here is my favorite one:

type DoQueryResult<T extends Query<A | B | C>> = T extends Query<infer V> ? V : never;

const doQuery = <T extends Query<A | B | C>>({ query }: { query: T }): DoQueryResult<T> => {
  return result as DoQueryResult<T>;
}

It has all benefits of the previous two solutions and just one drawback: you should typecast return statement in implementation (result as DoQueryResult<T>)

const doQuery = <T extends Query<A | B | C>>({ query }: { query: T }): DoQueryResult<T> => {
  const result = query.doTheQuery()

  const a = 6 + result; // It errors

  return result as DoQueryResult<T>; // It infers result as A | B | C and we need to typecast it
}
Related