diff --git a/src/cache/core/__tests__/cache.ts b/src/cache/core/__tests__/cache.ts index 5f07bb28e97..48660ffb715 100644 --- a/src/cache/core/__tests__/cache.ts +++ b/src/cache/core/__tests__/cache.ts @@ -2,7 +2,7 @@ import gql from 'graphql-tag'; import { ApolloCache } from '../cache'; import { Cache, DataProxy } from '../..'; import { Reference } from '../../../utilities/graphql/storeUtils'; - +import { expectTypeOf } from 'expect-type' class TestCache extends ApolloCache { constructor() { super(); @@ -308,3 +308,142 @@ describe('abstract cache', () => { }); }); }); + +describe.skip('Cache type tests', () => { + describe('modify', () => { + test('field types are inferred correctly from passed entity type', () => { + const cache = new TestCache(); + cache.modify<{ + prop1: string; + prop2: number; + child: { + someObject: true + }, + children: { + anotherObject: false + }[] + }>({ + fields: { + prop1(field) { + expectTypeOf(field).toEqualTypeOf(); + return field; + }, + prop2(field) { + expectTypeOf(field).toEqualTypeOf(); + return field; + }, + child(field) { + expectTypeOf(field).toEqualTypeOf<{ someObject: true } | Reference>(); + return field; + }, + children(field) { + expectTypeOf(field).toEqualTypeOf<(ReadonlyArray<{ anotherObject: false }>) | ReadonlyArray>(); + return field; + } + } + }) + }) + test('field method needs to return a value of the correct type', () => { + const cache = new TestCache(); + cache.modify<{ p1: string, p2: string, p3: string, p4: string, p5: string }>({ + fields: { + p1() { return "" }, + // @ts-expect-error returns wrong type + p2() { return 1 }, + // @ts-expect-error needs return statement + p3() {}, + p4(_, { DELETE }) { return DELETE }, + p5(_, { INVALIDATE }) { return INVALIDATE }, + } + }) + }) + test('passing a function as `field` should infer all entity properties as possible input (interfaces)', () => { + interface ParentEntity { + prop1: string; + prop2: number; + child: ChildEntity; + } + interface ChildEntity { + prop1: boolean; + prop2: symbol; + children: OtherChildEntry[]; + } + interface OtherChildEntry { + foo: false + } + + const cache = new TestCache(); + // with reference + cache.modify({ + id: 'foo', + fields(field) { + expectTypeOf(field).toEqualTypeOf(); + return field; + } + }) + // without reference + cache.modify({ + id: 'foo', + fields(field) { + expectTypeOf(field).toEqualTypeOf(); + return field; + } + }) + }) + test('passing a function as `field` should infer all entity properties as possible input (types)', () => { + type ParentEntity = { + prop1: string; + prop2: number; + child: ChildEntity; + } + type ChildEntity = { + prop1: boolean; + prop2: symbol; + children: OtherChildEntry[]; + } + type OtherChildEntry = { + foo: false + } + + const cache = new TestCache(); + // with reference + cache.modify({ + id: 'foo', + fields(field) { + expectTypeOf(field).toEqualTypeOf(); + return field; + } + }) + // without reference + cache.modify({ + id: 'foo', + fields(field) { + expectTypeOf(field).toEqualTypeOf(); + return field; + } + }) + }) + test('passing a function as `field` w/o specifying an entity type', () => { + const cache = new TestCache(); + cache.modify({ + id: 'foo', + fields(field) { + expectTypeOf(field).toEqualTypeOf(); + return field; + } + }); + }); + test('passing a function as `field` property w/o specifying an entity type', () => { + const cache = new TestCache(); + cache.modify({ + id: 'foo', + fields: { + p1(field) { + expectTypeOf(field).toEqualTypeOf(); + return field; + } + } + }); + }); + }); +}); diff --git a/src/cache/core/cache.ts b/src/cache/core/cache.ts index 742fd959921..e48f12ccaec 100644 --- a/src/cache/core/cache.ts +++ b/src/cache/core/cache.ts @@ -120,7 +120,7 @@ export abstract class ApolloCache implements DataProxy { return []; } - public modify>(options: Cache.ModifyOptions): boolean { + public modify = Record>(options: Cache.ModifyOptions): boolean { return false; } diff --git a/src/cache/core/types/Cache.ts b/src/cache/core/types/Cache.ts index 4efc0dd08a3..47504c287c7 100644 --- a/src/cache/core/types/Cache.ts +++ b/src/cache/core/types/Cache.ts @@ -1,5 +1,5 @@ import { DataProxy } from './DataProxy'; -import type { Modifier, Modifiers } from './common'; +import type { AllFieldsModifier, Modifiers } from './common';; import type { ApolloCache } from '../cache'; export namespace Cache { @@ -57,11 +57,9 @@ export namespace Cache { discardWatches?: boolean; } - export interface ModifyOptions> { + export interface ModifyOptions = Record> { id?: string; - fields: Entity extends Record - ? Modifiers | Modifier - : Modifier; + fields: Modifiers | AllFieldsModifier; optimistic?: boolean; broadcast?: boolean; } diff --git a/src/cache/core/types/common.ts b/src/cache/core/types/common.ts index 8be9c8c2c88..3a1cdcf4df6 100644 --- a/src/cache/core/types/common.ts +++ b/src/cache/core/types/common.ts @@ -98,17 +98,21 @@ export type Modifier = ( details: ModifierDetails ) => T | DeleteModifier | InvalidateModifier; -type StoreObjectValueMaybeReference = StoreVal extends Record< - string, - unknown ->[] - ? StoreVal | Reference[] - : StoreVal extends Record +type StoreObjectValueMaybeReference = + StoreVal extends Record[] + ? Readonly | readonly Reference[] + : StoreVal extends Record ? StoreVal | Reference : StoreVal; +export type AllFieldsModifier< + Entity extends Record +> = Modifier> + : never>; + export type Modifiers< - T extends Record = Record + T extends Record = Record > = Partial<{ [FieldName in keyof T]: Modifier< StoreObjectValueMaybeReference> diff --git a/src/cache/inmemory/__tests__/cache.ts b/src/cache/inmemory/__tests__/cache.ts index 9ead45a9ea4..84d8a42bd7b 100644 --- a/src/cache/inmemory/__tests__/cache.ts +++ b/src/cache/inmemory/__tests__/cache.ts @@ -2,7 +2,7 @@ import gql, { disableFragmentWarnings } from 'graphql-tag'; import { cloneDeep } from '../../../utilities/common/cloneDeep'; import { makeReference, Reference, makeVar, TypedDocumentNode, isReference, DocumentNode } from '../../../core'; -import { Cache, Modifiers } from '../../../cache'; +import { Cache } from '../../../cache'; import { InMemoryCache } from '../inMemoryCache'; import { InMemoryCacheConfig } from '../types'; @@ -2817,7 +2817,7 @@ describe("InMemoryCache#modify", () => { cache.modify({ fields: { - comments(comments: Reference[], { readField }) { + comments(comments: readonly Reference[], { readField }) { expect(Object.isFrozen(comments)).toBe(true); expect(comments.length).toBe(3); const filtered = comments.filter(comment => { @@ -2902,6 +2902,7 @@ describe("InMemoryCache#modify", () => { expect(fieldName).not.toBe("b"); if (fieldName === "a") expect(value).toBe(1); if (fieldName === "c") expect(value).toBe(3); + return value; }, optimistic: true, }); diff --git a/src/cache/inmemory/inMemoryCache.ts b/src/cache/inmemory/inMemoryCache.ts index f4cbaaa181d..49b33c59397 100644 --- a/src/cache/inmemory/inMemoryCache.ts +++ b/src/cache/inmemory/inMemoryCache.ts @@ -214,7 +214,7 @@ export class InMemoryCache extends ApolloCache { } } - public modify>(options: Cache.ModifyOptions): boolean { + public modify = Record>(options: Cache.ModifyOptions): boolean { if (hasOwn.call(options, "id") && !options.id) { // To my knowledge, TypeScript does not currently provide a way to // enforce that an optional property?:type must *not* be undefined