Skip to content

Commit

Permalink
fix(angular-query-experimental): allow returning undefined in initial…
Browse files Browse the repository at this point in the history
…Data function

Fix incorrect types that disallowed that. This is necessary because the initialData function can typically read cache synchronously and need to be able to signal to the query if there is a cache miss and an actual fetch needs to be deployed.

No breaking changes, any code that worked before should work now as well.
  • Loading branch information
ShacharHarshuv committed Apr 29, 2024
1 parent 7368bd0 commit fc00927
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 88 deletions.
Original file line number Diff line number Diff line change
@@ -1,127 +1,146 @@
import { assertType, describe, expectTypeOf } from 'vitest'
import { QueryClient } from '@tanstack/query-core'
import { dataTagSymbol } from '@tanstack/query-core'
import { queryOptions } from '../query-options'
import { injectQuery } from '../inject-query'
import type { Signal } from '@angular/core'

describe('queryOptions', () => {
test('should not allow excess properties', () => {
import { assertType, describe, expectTypeOf } from "vitest";
import { QueryClient } from "@tanstack/query-core";
import { dataTagSymbol } from "@tanstack/query-core";
import { queryOptions } from "../query-options";
import { injectQuery } from "../inject-query";
import type { Signal } from "@angular/core";

describe("queryOptions", () => {
test("should not allow excess properties", () => {
return queryOptions({
queryKey: ['key'],
queryKey: ["key"],
queryFn: () => Promise.resolve(5),
// @ts-expect-error this is a good error, because stallTime does not exist!
stallTime: 1000,
})
})
});
});

test('should infer types for callbacks', () => {
test("should infer types for callbacks", () => {
return queryOptions({
queryKey: ['key'],
queryKey: ["key"],
queryFn: () => Promise.resolve(5),
staleTime: 1000,
select: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
expectTypeOf(data).toEqualTypeOf<number>();
},
})
})
})

test('should work when passed to injectQuery', () => {
});
});

test("should allow undefined response in initialData", () => {
return (id: string | null) =>
queryOptions({
queryKey: ["todo", id],
queryFn: () =>
Promise.resolve({
id: "1",
title: "Do Laundry",
}),
initialData: () =>
!id
? undefined
: {
id,
title: "Initial Data",
},
});
});
});

test("should work when passed to injectQuery", () => {
const options = queryOptions({
queryKey: ['key'],
queryKey: ["key"],
queryFn: () => Promise.resolve(5),
})
});

const { data } = injectQuery(() => options)
expectTypeOf(data).toEqualTypeOf<Signal<number | undefined>>()
})
const { data } = injectQuery(() => options);
expectTypeOf(data).toEqualTypeOf<Signal<number | undefined>>();
});

test('should work when passed to fetchQuery', () => {
test("should work when passed to fetchQuery", () => {
const options = queryOptions({
queryKey: ['key'],
queryKey: ["key"],
queryFn: () => Promise.resolve(5),
})
});

const data = new QueryClient().fetchQuery(options)
assertType<Promise<number>>(data)
})
const data = new QueryClient().fetchQuery(options);
assertType<Promise<number>>(data);
});

test('should tag the queryKey with the result type of the QueryFn', () => {
test("should tag the queryKey with the result type of the QueryFn", () => {
const { queryKey } = queryOptions({
queryKey: ['key'],
queryKey: ["key"],
queryFn: () => Promise.resolve(5),
})
assertType<number>(queryKey[dataTagSymbol])
})
});
assertType<number>(queryKey[dataTagSymbol]);
});

test('should tag the queryKey even if no promise is returned', () => {
test("should tag the queryKey even if no promise is returned", () => {
const { queryKey } = queryOptions({
queryKey: ['key'],
queryKey: ["key"],
queryFn: () => 5,
})
assertType<number>(queryKey[dataTagSymbol])
})
});
assertType<number>(queryKey[dataTagSymbol]);
});

test('should tag the queryKey with unknown if there is no queryFn', () => {
test("should tag the queryKey with unknown if there is no queryFn", () => {
const { queryKey } = queryOptions({
queryKey: ['key'],
})
queryKey: ["key"],
});

assertType<unknown>(queryKey[dataTagSymbol])
})
assertType<unknown>(queryKey[dataTagSymbol]);
});

test('should tag the queryKey with the result type of the QueryFn if select is used', () => {
test("should tag the queryKey with the result type of the QueryFn if select is used", () => {
const { queryKey } = queryOptions({
queryKey: ['key'],
queryKey: ["key"],
queryFn: () => Promise.resolve(5),
select: (data) => data.toString(),
})
});

assertType<number>(queryKey[dataTagSymbol])
})
assertType<number>(queryKey[dataTagSymbol]);
});

test('should return the proper type when passed to getQueryData', () => {
test("should return the proper type when passed to getQueryData", () => {
const { queryKey } = queryOptions({
queryKey: ['key'],
queryKey: ["key"],
queryFn: () => Promise.resolve(5),
})
});

const queryClient = new QueryClient()
const data = queryClient.getQueryData(queryKey)
const queryClient = new QueryClient();
const data = queryClient.getQueryData(queryKey);

expectTypeOf(data).toEqualTypeOf<number | undefined>()
})
expectTypeOf(data).toEqualTypeOf<number | undefined>();
});

test('should properly type updaterFn when passed to setQueryData', () => {
test("should properly type updaterFn when passed to setQueryData", () => {
const { queryKey } = queryOptions({
queryKey: ['key'],
queryKey: ["key"],
queryFn: () => Promise.resolve(5),
})
});

const queryClient = new QueryClient()
const queryClient = new QueryClient();
const data = queryClient.setQueryData(queryKey, (prev) => {
expectTypeOf(prev).toEqualTypeOf<number | undefined>()
return prev
})
expectTypeOf(prev).toEqualTypeOf<number | undefined>();
return prev;
});

expectTypeOf(data).toEqualTypeOf<number | undefined>()
})
expectTypeOf(data).toEqualTypeOf<number | undefined>();
});

test('should properly type value when passed to setQueryData', () => {
test("should properly type value when passed to setQueryData", () => {
const { queryKey } = queryOptions({
queryKey: ['key'],
queryKey: ["key"],
queryFn: () => Promise.resolve(5),
})
});

const queryClient = new QueryClient()
const queryClient = new QueryClient();

// @ts-expect-error value should be a number
queryClient.setQueryData(queryKey, '5')
queryClient.setQueryData(queryKey, "5");
// @ts-expect-error value should be a number
queryClient.setQueryData(queryKey, () => '5')
queryClient.setQueryData(queryKey, () => "5");

const data = queryClient.setQueryData(queryKey, 5)
const data = queryClient.setQueryData(queryKey, 5);

expectTypeOf(data).toEqualTypeOf<number | undefined>()
})
expectTypeOf(data).toEqualTypeOf<number | undefined>();
});
29 changes: 17 additions & 12 deletions packages/angular-query-experimental/src/query-options.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import type { DataTag, DefaultError, QueryKey } from '@tanstack/query-core'
import type { CreateQueryOptions } from './types'
import {
DataTag,
DefaultError,
QueryKey,
InitialDataFunction,
} from "@tanstack/query-core";
import type { CreateQueryOptions } from "./types";

export type UndefinedInitialDataOptions<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = CreateQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
initialData?: undefined
}
initialData?: undefined;
};

type NonUndefinedGuard<T> = T extends undefined ? never : T
type NonUndefinedGuard<T> = T extends undefined ? never : T;

export type DefinedInitialDataOptions<
TQueryFnData = unknown,
Expand All @@ -20,8 +25,8 @@ export type DefinedInitialDataOptions<
> = CreateQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
initialData:
| NonUndefinedGuard<TQueryFnData>
| (() => NonUndefinedGuard<TQueryFnData>)
}
| InitialDataFunction<NonUndefinedGuard<TQueryFnData>>;
};

export function queryOptions<
TQueryFnData = unknown,
Expand All @@ -31,8 +36,8 @@ export function queryOptions<
>(
options: UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
): UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> & {
queryKey: DataTag<TQueryKey, TQueryFnData>
}
queryKey: DataTag<TQueryKey, TQueryFnData>;
};

export function queryOptions<
TQueryFnData = unknown,
Expand All @@ -42,9 +47,9 @@ export function queryOptions<
>(
options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
): DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> & {
queryKey: DataTag<TQueryKey, TQueryFnData>
}
queryKey: DataTag<TQueryKey, TQueryFnData>;
};

export function queryOptions(options: unknown) {
return options
return options;
}

0 comments on commit fc00927

Please sign in to comment.