Testowanie typów
Przykładowy Projekt
Vitest umożliwia pisanie testów typów za pomocą składni expectTypeOf
lub assertType
. Domyślnie wszystkie pliki z rozszerzeniem *.test-d.ts
są traktowane jako testy typów, ale można to zmienić za pomocą opcji konfiguracyjnej typecheck.include
.
W tle Vitest uruchamia tsc
lub vue-tsc
(w zależności od konfiguracji) i analizuje wyniki. Vitest wyświetli również błędy typów w kodzie źródłowym, jeśli zostaną znalezione. Można to wyłączyć za pomocą opcji konfiguracyjnej typecheck.ignoreSourceErrors
.
Należy pamiętać, że Vitest nie wykonuje ani nie kompiluje tych plików. Są one jedynie statycznie analizowane przez kompilator, dlatego nie można używać dynamicznych instrukcji. Oznacza to, że nie można używać dynamicznych nazw testów ani API takich jak test.each
, test.runIf
, test.skipIf
, test.concurrent
. Można jednak używać innych API, takich jak test
, describe
, .only
, .skip
i .todo
.
Używanie flag CLI, takich jak --allowOnly
i -t
, jest również obsługiwane podczas sprawdzania typów.
import { assertType, expectTypeOf } from 'vitest';
import { mount } from './mount.js';
test('my types work properly', () => {
expectTypeOf(mount).toBeFunction();
expectTypeOf(mount).parameter(0).toMatchTypeOf<{ name: string }>();
// @ts-expect-error name is a string
assertType(mount({ name: 42 }));
});
Każdy błąd typu w pliku testowym będzie traktowany jako błąd testu, co pozwala na wykorzystanie dowolnych technik związanych z typami do testowania typów w projekcie.
Listę dostępnych matcherów można znaleźć w sekcji API.
Interpretacja błędów
W przypadku korzystania z API expectTypeOf
, warto zapoznać się z dokumentacją expect-type dotyczącą komunikatów o błędach.
Gdy typy nie pasują, .toEqualTypeOf
i .toMatchTypeOf
wykorzystują specjalny typ pomocniczy do generowania komunikatów o błędach, które mają być jak najbardziej pomocne. Należy jednak pamiętać o pewnym niuansie. Ponieważ asercje są zapisywane w sposób "płynny" (fluent), błąd powinien odnosić się do typu "oczekiwanego", a nie typu "rzeczywistego" (expect<Actual>().toEqualTypeOf<Expected>()
). Oznacza to, że błędy typów mogą być nieco mylące – dlatego ta biblioteka generuje typ MismatchInfo
, aby spróbować wyjaśnić, czego się oczekuje. Na przykład:
expectTypeOf({ a: 1 }).toEqualTypeOf<{ a: string }>();
Ta asercja zakończy się niepowodzeniem, ponieważ {a: 1}
ma typ {a: number}
, a nie {a: string}
. Komunikat o błędzie w tym przypadku będzie wyglądał mniej więcej tak:
test/test.ts:999:999 - error TS2344: Type '{ a: string; }' does not satisfy the constraint '{ a: \\"Expected: string, Actual: number\\"; }'.
Types of property 'a' are incompatible.
Type 'string' is not assignable to type '\\"Expected: string, Actual: number\\"'.
999 expectTypeOf({a: 1}).toEqualTypeOf<{a: string}>()
Zauważ, że zgłoszony typ ograniczenia jest komunikatem zrozumiałym dla człowieka, określającym zarówno typ "oczekiwany", jak i "rzeczywisty". Zamiast dosłownie interpretować zdanie Types of property 'a' are incompatible // Type 'string' is not assignable to type "Expected: string, Actual: number"
– po prostu spójrz na nazwę właściwości ('a'
) i komunikat: Expected: string, Actual: number
. To powinno wskazać, co jest nie tak, w większości przypadków. Bardziej złożone typy będą oczywiście wymagały więcej wysiłku podczas debugowania i mogą wymagać pewnych eksperymentów. Jeśli komunikaty o błędach są rzeczywiście mylące, proszę zgłosić problem.
Metody typu toBe...
(takie jak toBeString
, toBeNumber
, toBeVoid
itp.) kończą się niepowodzeniem, rozwiązując się do typu niewywoływalnego, gdy testowany typ Actual
nie pasuje. Na przykład, błąd dla asercji takiej jak expectTypeOf(1).toBeString()
będzie wyglądał mniej więcej tak:
test/test.ts:999:999 - error TS2349: This expression is not callable.
Type 'ExpectString<number>' has no call signatures.
999 expectTypeOf(1).toBeString()
~~~~~~~~~~
Fragment This expression is not callable
nie jest zbyt pomocny – istotny błąd to następna linia: Type 'ExpectString<number> has no call signatures
. Oznacza to, że przekazano liczbę, ale oczekiwano ciągu znaków.
Gdyby TypeScript dodał obsługę "throw" types, te komunikaty o błędach mogłyby zostać znacznie ulepszone. Do tego czasu będą wymagały dokładnej analizy.
Konkretne obiekty 'oczekiwane' a argumenty typów
Komunikaty o błędach dla asercji takiej jak ta:
expectTypeOf({ a: 1 }).toEqualTypeOf({ a: '' });
Będą mniej pomocne niż dla asercji takiej jak ta:
expectTypeOf({ a: 1 }).toEqualTypeOf<{ a: string }>();
Dzieje się tak, ponieważ kompilator TypeScript musi wywnioskować argument typu w pierwszym przypadku, a ta biblioteka może oznaczyć go jako błąd tylko przez porównanie go z ogólnym typem Mismatch
. Dlatego zaleca się używanie argumentów typów zamiast konkretnego typu dla .toEqualTypeOf
i .toMatchTypeOf
, o ile to możliwe. Jeśli porównanie dwóch konkretnych typów jest znacznie wygodniejsze, można użyć typeof
:
const one = valueFromFunctionOne({ some: { complex: inputs } });
const two = valueFromFunctionTwo({ some: { other: inputs } });
expectTypeOf(one).toEqualTypeOf<typeof two>();
Jeśli masz trudności z pracą z API expectTypeOf
i rozwiązywaniem błędów, zawsze możesz użyć prostszego API assertType
:
const answer = 42;
assertType<number>(answer);
// @ts-expect-error answer is not a string
assertType<string>(answer);
TIP
Używając składni @ts-expect-error
, warto upewnić się, że nie popełniłeś literówki. Aby to zrobić, dodaj pliki typów do opcji konfiguracyjnej test.include
. Wtedy Vitest uruchomi te testy i w przypadku błędu zgłosi ReferenceError
.
Test ten zostanie zaliczony, ponieważ oczekiwany jest błąd, ale słowo "answer" zawiera literówkę, co skutkuje fałszywie pozytywnym wynikiem:
// @ts-expect-error answer is not a string
assertType<string>(answr); //
Uruchamianie sprawdzania typów
Od Vitest 1.0, aby włączyć sprawdzanie typów, wystarczy dodać flagę --typecheck
do polecenia Vitest w package.json
:
{
"scripts": {
"test": "vitest --typecheck"
}
}
Teraz możesz uruchomić sprawdzanie typów:
npm run test
yarn test
pnpm run test
bun test
Vitest używa tsc --noEmit
lub vue-tsc --noEmit
, w zależności od konfiguracji, więc możesz usunąć te skrypty z procesu budowania.