Test throws errors TS2322 and TS2769 when adding an additional unused date field to a custom type

I’m using

  • Pop!_OS 21.04 (Ubuntu)
  • node v16.11.1
  • npm v7.5.2
  • Vue CLI v4.5.13

and want to create a Vue3 app using the Pinia package until Vuex5 will be ready. I run

vue create foo

# manually select TypeScript and Jest for unit tests

cd foo

npm install pinia

Inside the generated example.spec.ts file I setup a custom type, a sample store and a single test

import { defineStore, setActivePinia, createPinia } from "pinia";
import { computed, ComputedRef, ref, Ref } from "vue";

/*
    Define a custom type for demo purposes
*/
type MyValue = { isTrue: boolean };

/*
    Define the store using the composition API
*/
const useStore = defineStore("myValues", () => {
  const myValues: Ref<MyValue[]> = ref<MyValue[]>([]);

  const myTruthyValues: ComputedRef<MyValue[]> = computed<MyValue[]>(() =>
    myValues.value.filter((myValue: MyValue) => myValue.isTrue));

  return { myTruthyValues };
});

/*
    Use the store somewhere in the project
*/
describe("Store", (): void => {
  beforeEach(() => {
    setActivePinia(createPinia());
  });

  it("returns truthy values", (): void => {
    const myValuesStore = useStore();

    expect(myValuesStore.myTruthyValues.every((myValue: MyValue) => myValue.isTrue)).toBeTruthy();
  });
});

The test passes. But when changing the type definition of MyValue to type MyValue = { isTrue: boolean; createdAt: Date; }; the test fails with

TypeScript diagnostics (customize using `[jest-config].globals.ts-jest.diagnostics` option):
tests/unit/example.spec.ts:13:9 - error TS2322: Type 'Ref<{ isTrue: boolean; createdAt: { toString: () => string; toDateString: () => string; toTimeString: () => string; toLocaleString: { (): string; (locales?: string | string[] | undefined, options?: DateTimeFormatOptions | undefined): string; }; ... 39 more ...; getVarDate: () => VarDate; }; }[]>' is not assignable to type 'Ref<MyValue[]>'.
  Type '{ isTrue: boolean; createdAt: { toString: () => string; toDateString: () => string; toTimeString: () => string; toLocaleString: { (): string; (locales?: string | string[] | undefined, options?: DateTimeFormatOptions | undefined): string; }; ... 39 more ...; getVarDate: () => VarDate; }; }[]' is not assignable to type 'MyValue[]'.
    Type '{ isTrue: boolean; createdAt: { toString: () => string; toDateString: () => string; toTimeString: () => string; toLocaleString: { (): string; (locales?: string | string[] | undefined, options?: DateTimeFormatOptions | undefined): string; }; ... 39 more ...; getVarDate: () => VarDate; }; }' is not assignable to type 'MyValue'.
      Types of property 'createdAt' are incompatible.
        Property '[Symbol.toPrimitive]' is missing in type '{ toString: () => string; toDateString: () => string; toTimeString: () => string; toLocaleString: { (): string; (locales?: string | string[] | undefined, options?: DateTimeFormatOptions | undefined): string; }; ... 39 more ...; getVarDate: () => VarDate; }' but required in type 'Date'.

13   const myValues: Ref<MyValue[]> = ref<MyValue[]>([]);
           ~~~~~~~~

  node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts:114:5
    114     [Symbol.toPrimitive](hint: "default"): string;
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    '[Symbol.toPrimitive]' is declared here.
tests/unit/example.spec.ts:32:47 - error TS2769: No overload matches this call.
  Overload 1 of 2, '(predicate: (value: { isTrue: boolean; createdAt: { toString: () => string; toDateString: () => string; toTimeString: () => string; toLocaleString: { (): string; (locales?: string | string[] | undefined, options?: DateTimeFormatOptions | undefined): string; }; ... 39 more ...; getVarDate: () => VarDate; }; }, index: number, array: { ...; }[]) => value is { ...; }, thisArg?: any): this is { ...; }[]', gave the following error.
    Argument of type '(myValue: MyValue) => boolean' is not assignable to parameter of type '(value: { isTrue: boolean; createdAt: { toString: () => string; toDateString: () => string; toTimeString: () => string; toLocaleString: { (): string; (locales?: string | string[] | undefined, options?: DateTimeFormatOptions | undefined): string; }; ... 39 more ...; getVarDate: () => VarDate; }; }, index: number, arr...'.
      Types of parameters 'myValue' and 'value' are incompatible.
        Type '{ isTrue: boolean; createdAt: { toString: () => string; toDateString: () => string; toTimeString: () => string; toLocaleString: { (): string; (locales?: string | string[] | undefined, options?: DateTimeFormatOptions | undefined): string; }; ... 39 more ...; getVarDate: () => VarDate; }; }' is not assignable to type 'MyValue'.
          Types of property 'createdAt' are incompatible.
            Property '[Symbol.toPrimitive]' is missing in type '{ toString: () => string; toDateString: () => string; toTimeString: () => string; toLocaleString: { (): string; (locales?: string | string[] | undefined, options?: DateTimeFormatOptions | undefined): string; }; ... 39 more ...; getVarDate: () => VarDate; }' but required in type 'Date'.
  Overload 2 of 2, '(predicate: (value: { isTrue: boolean; createdAt: { toString: () => string; toDateString: () => string; toTimeString: () => string; toLocaleString: { (): string; (locales?: string | string[] | undefined, options?: DateTimeFormatOptions | undefined): string; }; ... 39 more ...; getVarDate: () => VarDate; }; }, index: number, array: { ...; }[]) => unknown, thisArg?: any): boolean', gave the following error.
    Argument of type '(myValue: MyValue) => boolean' is not assignable to parameter of type '(value: { isTrue: boolean; createdAt: { toString: () => string; toDateString: () => string; toTimeString: () => string; toLocaleString: { (): string; (locales?: string | string[] | undefined, options?: DateTimeFormatOptions | undefined): string; }; ... 39 more ...; getVarDate: () => VarDate; }; }, index: number, arr...'.
      Types of parameters 'myValue' and 'value' are incompatible.
        Type '{ isTrue: boolean; createdAt: { toString: () => string; toDateString: () => string; toTimeString: () => string; toLocaleString: { (): string; (locales?: string | string[] | undefined, options?: DateTimeFormatOptions | undefined): string; }; ... 39 more ...; getVarDate: () => VarDate; }; }' is not assignable to type 'MyValue'.

32     expect(myValuesStore.myTruthyValues.every((myValue: MyValue) => myValue.isTrue)).toBeTruthy();
                                                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

  node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts:114:5
    114     [Symbol.toPrimitive](hint: "default"): string;
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    '[Symbol.toPrimitive]' is declared here.

The additional date field should not affect the test, although it seems to struggle with that type because an additional number field works fine.

Unfortunately I don’t know what’s wrong

  • My code? (but the test passes without the date field)
  • Vue provides a wrong TS config?
  • Jest struggles with it?

I don’t think this issue is related to Pinia…

Any ideas?

Answer

This is a limitation of TypeScript, as described in an answer to a similar question. However, the proposed solution there does not work for some reason.

Option 1: Update typescript to 4.3+

The TypeScript issue is fixed in 4.3, so perhaps the quickest solution is to update your typescript version:

npm i -D typescript@^4.4.3

Option 2: Omit Symbol.toPrimitive

If you prefer to keep the older version of typescript, you can omit Symbol.toPrimitive from Date for the createdAt property type:

type MyValue = { isTrue: boolean, createdAt: Omit<Date, SymbolConstructor['toPrimitive']> };

Option 3: Use type inference

A workaround that avoids the type conflict is to lean on type inference instead of typing everything. In the example below, only the myValues ref is explicitly typed, while all related variable types are inferred ๐Ÿ’ก:

import { defineStore, setActivePinia, createPinia } from "pinia";
import { computed, ref } from "vue";

type MyValue = { isTrue: boolean; createdAt: Date; };

const useStore = defineStore("myValues", () => {
           ๐Ÿ’ก             ๐Ÿ‘‡
  const myValues = ref<MyValue[]>([]);
             ๐Ÿ’ก
  const myTruthyValues = computed(() =>
                             ๐Ÿ’ก
  myValues.value.filter(myValue => myValue.isTrue));

  return { myTruthyValues };
});

describe("Store", (): void => {
  beforeEach(() => {
    setActivePinia(createPinia());
  });

  it("returns truthy values", (): void => {
    const myValuesStore = useStore();
                                                 ๐Ÿ’ก
    expect(myValuesStore.myTruthyValues.every(myValue => myValue.isTrue)).toBeTruthy();
  });
});