#Type inferrence on function output is lost when parameter is defined

30 messages · Page 1 of 1 (latest)

ionic jackal
#

I'm not too sure how to describe the issue I'm facing, but here's a concrete example :

I have this defineTable function that accepts a tanstack query option definiton, and I want to infer the data returned on the property results of this query.

import type { QueryKey, QueryOptions } from '@tanstack/vue-query';
export type Primitive = string | number | symbol;
export type GenericObject = Record<Primitive, unknown>;
type MaybePromise<T> = T | Promise<T>;


type QueryOption = QueryOptions<{ count: number; total: number; results: any[] }>;

type QueryData<QOpt extends QueryOption> = QOpt['queryFn'] extends () => MaybePromise<infer T>
  ? T extends { results: Array<infer U> }
    ? U
    : never
  : never;

export type FetchParams = {
  sort: string;
  page: number;
  perPage: number;
};

export type DataListSchema<
  Opts extends QueryOption,
  Data extends QueryData<Opts>,
  Query extends (params: FetchParams) => Opts = (params: FetchParams) => Opts,
> = {
  query: Query;
  data: Data;
};

function defineTable<Opts extends QueryOption, Data extends QueryData<Opts>>(
  schema: DataListSchema<Opts, Data>,
) {}

This works fine, when I do this :

defineTable({
  // NO PARAMS
  query: () => ({
    queryKey: ['table', 'data'],
    queryFn: () =>
      Promise.resolve({
        count: 10,
        total: 100,
        results: [
          { id: 1, name: 'John' },
          { id: 2, name: 'Jane' },
        ],
      }),
  }),
  // TYPE IS PROPERLY INFERRED
  data: { id: 1, name: true   },
});

But when I try to use the parameter of the query function, the inferred data type becomes "never"


defineTable({
  // PARAMS IS USED
  query: (params) => ({
    queryKey: ['table', 'data', params],
    queryFn: () =>
      Promise.resolve({
        count: 10,
        total: 100,
        results: [
          { id: 1, name: 'John' },
          { id: 2, name: 'Jane' },
        ],
      })
  }),
  // TYPE INFERRENCE LOST
  data: { id: 1, name: true   },
});

lone echoBOT
#
chronicstone#0

Preview:```ts
import type {
QueryKey,
QueryOptions,
} from "@tanstack/vue-query"

export type Primitive = string | number | symbol
export type GenericObject = Record<Primitive, unknown>
type MaybePromise<T> = T | Promise<T>

type QueryOption = QueryOptions<{
count: number
total: number
results: any[]
}>
...```

ionic jackal
#

!help

lone echoBOT
#
TypeScript Community
Bot Usage

Hello chronicstone! Here is a list of all commands in me! To get detailed description on any specific command, do help <command>

**Help System Commands:**

helper ► Ping the @Helper role from a help post
resolved ► Mark a post as resolved
reopen ► Reopen a resolved post

**Misc Commands:**

ping ► See if the bot is alive
playground ► Shorten a TypeScript playground link
help ► Sends what you're looking at right now
handbook ► Search the TypeScript Handbook

**Reputation Commands:**

rep ► Give a different user some reputation points
history ► View a user's reputation history
leaderboard ► See who has the most reputation

**Snippet Commands:**

listSnippets ► List snippets matching an optional filter
snip ► Create or edit a snippet
deleteSnip ► Delete a snippet you own

**Twoslash Commands:**

twoslash ► Run twoslash on the latest codeblock, optionally returning the quick infos of specified symbols. You can use ts@4.8.3 or ts@next to run a specific version.

ionic jackal
#

!helper

honest locust
#

in the first one, the type of Query is being inferred correctly,

#

second one it doesn't try and is using the initializer

ionic jackal
#

@honest locust Yep I've seen this, but I don't understand the reason behind this behaviour. Not the first time I have to deal with this strange case where adding a param defined in the expected type just breaks inferrence

ionic jackal
#

!helper

#

Sorry for the extra ping, been pulling my hair for quite a while on this one and can't figure out why TS behaves like this...

neat elbow
#

STOP

#

ayways there are a lot of things that can be improved here

#

first up, WTF is that font

#

use jetbrains mono

#

next up

#

whats that theme

#

it sucks

#

use some themes in marcketplace

tribal falcon
#

never comes from QueryData, so have you looked into why the extends fails?

#

You seem to be misusing generic type parameters as a place to create types. This is a bad idea, because the default types that generally used could be overwridden by explictly specifying those types, and this adds an extra level of complexity to type analysis

#

And it just makes it harder to read the code

#

If you just explicitly type params, it works

#
defineTable({
  // PARAMS IS USED
  query: (params: FetchParams) => ({
    queryKey: ['table', 'data', params],
    queryFn: () =>
      Promise.resolve({
        count: 10,
        total: 100,
        results: [
          { id: 1, name: 'John' },
          { id: 2, name: 'Jane' },
        ],
      }),
  }),
  data: { id: 1, name: true   },
});
#

This also works:

// cleaned up some types
export type DataListSchema<
  Opts extends QueryOption,
> = {
  query: (params: FetchParams) => Opts;
  data: QueryData<Opts>;
};

function defineTable<Opts extends QueryOption>(
  schema: DataListSchema<Opts>,
) {}

// new
type O = {
    queryKey: (string | FetchParams)[];
    queryFn: () => Promise<{
        count: number;
        total: number;
        results: {
            id: number;
            name: string;
        }[];
    }>;
}

            // Explicit Opts type
defineTable<O>({
  query: (params) => ({
    queryKey: ['table', 'data', params],
    queryFn: () =>
      Promise.resolve({
        count: 10,
        total: 100,
        results: [
          { id: 1, name: 'John' },
          { id: 2, name: 'Jane' },
        ],
      }),
  }),
  data: { id: 1, name: true   },
});
#

TS seems to have trouble inferring the type of query, when it's params lacks an explicit type. Maybe because it can't infer a specific type when the data you gave it is not fully typed. It gives up and infers query as the basic constraint of QueryOption, which grants the type of params as FetchParams, but loses the rest of the information.

#

And QueryOption fails your QueryData extends check, because it is looser than the data you are actually passing

#

It's a good idea to explicitly type things where you can, in my opinion. Trying to get TS to infer complex multi leveled types that are feeding types in and taking types out of various places just seems like asking for trouble IMO, when you can provide an explicit type and avoid that complexity.

#

You can post it up a TS issues on github as a feature request if you want to ask for improved inferrence. It seems possible for TS to do it, but also seems quite complicated to me, not sure I'd want to try to figure out the logic for that personally.

ionic jackal
#

@tribal falcon Hi, forgot to answer but thanks SO much, I wasn't familiar with that behaviour. Explicit params type solves all the cases in which I encountered the issue

fallen sail
# neat elbow first up, WTF is that font

This is not helpful to someone asking a question. If you're going to participate in help threads, please try to answer the question rather than criticizing their font and theme.