https://github.com/gregros/declare-it
Accurately test type declarations.
https://github.com/gregros/declare-it
library package testing type-declarations typescript
Last synced: about 1 month ago
JSON representation
Accurately test type declarations.
- Host: GitHub
- URL: https://github.com/gregros/declare-it
- Owner: GregRos
- License: mit
- Created: 2024-04-01T16:31:34.000Z (about 1 year ago)
- Default Branch: master
- Last Pushed: 2025-04-01T15:16:45.000Z (2 months ago)
- Last Synced: 2025-05-06T23:43:14.087Z (about 1 month ago)
- Topics: library, package, testing, type-declarations, typescript
- Language: TypeScript
- Homepage:
- Size: 11.6 MB
- Stars: 1
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE.md
Awesome Lists containing this project
README
# declare-it
[](https://www.npmjs.com/package/declare-it)
[](https://www.npmjs.com/package/declare-it)
[](https://github.com/gregros/declare-it/actions/workflows/push.yaml)Test your TypeScript type declarations with style!
Plugs into your favorite runtime test framework.
- ๐ทโโ๏ธ Write actual test cases, with titles and everything!
- ๐ **Simple** but **incredibly accurate** type assertions
- ๐งผ A clean and legible API, with a dash of DevEx magic.
- ๐ Human-readable compile-time errors!
- ๐ช Keeps track of tests by registering them with your test framework!
- ๐โโ๏ธ No plugins or configuration required!
Hereโs what it looks like:
```ts
import {declare, type, type_of} from "declare-it"declare.it("tests basic math", expect => {
expect( type<1> ).to_subtype( type )expect(
// โ type being compared:
type<1>
// โ type assertion:
).to_subtype(
// โ comparing against:
type
// โ chain another assertion:
).and.to_subtype(
// โ infer the type of a value:
type_of(val)
)
})
```Get it now!
```bash
yarn add -D declare-it
``````bash
npm install --save-dev declare-it
```
# Declaring test cases
You declare test cases using the `declare.it` function, which takes a callback that has one parameter.This is your `expect` function. Itโs what you use to make assertions about types. Every `expect` function belongs to a test case and knows its title.
```ts
declare.it("your test title", expect => {
expect(...).to_equal(...)
})
```**These test cases donโt actually execute at runtime.** Your test runner is the compiler, and it โrunsโ your tests by compiling your code.
That also means these test cases can contain any code you want. They can be async too. It doesnโt matter; it just has to compile.
```ts
declare.it("side-effects?", async expect => {
let x = await callSomeFunction()
expect(type_of(x)).to_equal(type<1>)
})
```
# Referencing types
When using **declare-it**, to make assertions about types you need to reference them in a special way.There are two options:
- Using the explicit `type`
- Using inferred `type_of(yourValue)`So either specify the type explicitly:
```ts
type
type<1>
type
```Or infer it from a variable:
```ts
type_of(variable)
```Just donโt use it with literals:
```ts
type_of("hello world")
```Literals donโt have declared types, so the type you get might not be what you expect.
# Being assertive
**declare-it**โs type assertions all work and look the same. Theyโre methods on the `expect` object you get by calling:```ts
expect(
type
)
```They all start with `to_`, such as:
```ts
// Inside a declare.it clause:
expect(
type<1>
).to_subtype(
type
)
```They can all be inverted by using `.not`, like this:
```ts
// Inside a declare.it clause:
expect(
type<1>
).not.to_subtype(
type
)
```And you can also chain them by tacking `.and` like this:
```ts
// Inside a declare.it clause:
expect(
type<1>
).to_subtype(
type
).and.to_subtype(
type
)
```You can do both, but youโll need to prefix every inverted assertion with `not.` for the sake of readability:
```ts
// Inside a declare.it clause:
expect(
type<1>
).not.to_subtype(
type<2>
).and.not.to_subtype(
type<3>
)
```Letโs take a look at the assertions you can make.
# to_equal [ L โก R ]
This the strictest assertion **declare-it** has in its arsenal. It checks if two types are **interchangeable**.It will only pass if you can replace one type with another *in all contexts*. Any code that compiles using one of them has to also compile with the other.
That means identical modifiers on properties:
```ts
expect(
type<{a: 1}>
).not.to_equal(
type<{readonly a: 1}>
).and.not.to_equal(
type<{a: 1}>
)
```Identical key declarations:
```ts
expect(
type<{1: 1}>
).not.to_equal(
type<{"1": 1}>
)
```Identical call signatures:
```ts
expect(
type<() => 1>
).not.to_equal(
type< () => 1 >
)
```And everything else!
# to_subtype [ L โ R ]
This assertion checks if one type `L` is a **subtype of** another type `R`. This means:- `L` has all of the *structure* of `R`, like members, call signatures, and so on.
- A value of type `L` can be assigned to a variable of type `R`.
- And finally, you can use `L` instead of `R` in generic type constraints.That means this code has to compile:
```ts
const right: R = null! as L
```But this code has to compile too:
```ts
type Subtype_Of = null
type L_Subtypes_R = Subtype_Of
```This is an extremely useful assertion. By constructing the right type to compare against, you can make all kinds of complex statements about the type being tested.
For example, you can check your type has a specific property using:
```ts
expect(
type
).to_subtype(
type< {yourKey: YourValue } >
)
```
## Negation [ L โ R ]
The negation is also quite useful, as it lets you make sure a type *doesnโt* have some structure you donโt want, like an indexer:```ts
expect(...).not.to_subtype(
type<{
[x: string]: unknown
}>
)
```You can also use it to make sure one of your methods *isnโt* callable with a set of types:
```ts
expect(
type
).not.to_subtype(
type<{
method(x: number): unknown
}>
)
```
# to_supertype [ L โ R ]
This assertion checks the opposite โ that `L` is a supertype of `R`. This means:- `R` has all of the structure of `L`
- A value of type `R` is assignable to a variable of type `L`
- You can use `R` instead of `L` to satisfy type constraints.Itโs basically the same check as `to_subtype`, but with the operands inverted.
# to_resemble [ L โ R ]
This combines the two previous assertions. It can also be written as:Or as:
```ts
expect(
type
).to_subtype(
type
).and.to_supertype(
type
)
```In other words, it lets you check whether two types **are mutual subtypes of each other**, having the same structure.
In particular, the following code has to compile:
```ts
const right: R = null! as L
const left: L = null! as R
```As well as the following code:
```ts
type L_Subtypes_R = Subtype_Of
type R_Subtypes_L = Subtype_Of
```Which tells you that the right-hand type is assignable to the left-hand one. However, itโs not as strict or accurate as `to_equal`.
## Negation [ L โ R ]
The negation โ `not.to_resemble` โ means two types arenโt the same. One way to use it is to check that a type isnโt `any`:```ts
expect(
type
).not.to_resemble(
type
)
```
# Dealing with failure
Hereโs what TypeScript says when an assertion fails:```
src/test/core/primitives.spec.ts:14:33 - error TS2345:
Argument of type
'{ (): (_: never) => number; (_: never): number; }'
is not assignable to parameter of type
'[" ",
"โ ๐๐ง ๐ง๐๐ฆ๐ง โ1 โ numberโ โฑโค
๐ง๐๐ ๐ง๐ฌ๐ฃ๐ (", 1, ") ๐๐ข๐๐ฆ ๐ก๐ข๐ง ๐ฆ๐จ๐ฃ๐๐ฅ-๐ง๐ฌ๐ฃ๐ (", number, ") "]'.
```Itโs a failure message with some garbage at the start! Letโs take a closer look, without the unnecessary characters:
```
โ ๐๐ง ๐ง๐๐ฆ๐ง โ1 โ numberโ โฑโค
๐ง๐๐ ๐ง๐ฌ๐ฃ๐ (", 1, ") ๐๐ข๐๐ฆ ๐ก๐ข๐ง ๐ฆ๐จ๐ฃ๐๐ฅ-๐ง๐ฌ๐ฃ๐ (", number, ")
```Here we can see:
1. A big red โ, always reassuring.
2. Bold text ๐ฎ
3. The name of the test where the failure happened.
4. Badass Unicode arrow thingy.
5. The `L` type โ the one that went into the `expect` function.
6. The `R` type โ the one that went into the assertion function.
7. A description of the problem.And of course, you also have the trace pointing to the line where the failure occurred.
Use this information wisely!
# Automagic registration
**declare-it** comes with a bonus feature. It will actually talk to your test framework โ provided you have one โ and tell it about the tests youโre making.The tests still run as part of compilation, but youโll have a pretty list so you can feel proud of yourself for writing them. Hereโs how it looks like in Jest:
```
PASS src/test/core/any.spec.ts
โ ๐ญ ๐ง๐ฌ๐ฃ๐-๐ข๐ก๐๐ฌ ๐ง๐๐ฆ๐ง: any is only equal to any (1 ms)
โ ๐ญ ๐ง๐ฌ๐ฃ๐-๐ข๐ก๐๐ฌ ๐ง๐๐ฆ๐ง: {a: any} โก {a: any}
โ ๐ญ ๐ง๐ฌ๐ฃ๐-๐ข๐ก๐๐ฌ ๐ง๐๐ฆ๐ง: {a: any} โ {a: 1}
โ ๐ญ ๐ง๐ฌ๐ฃ๐-๐ข๐ก๐๐ฌ ๐ง๐๐ฆ๐ง: {a: any, b: any} โ {a: any}
โ ๐ญ ๐ง๐ฌ๐ฃ๐-๐ข๐ก๐๐ฌ ๐ง๐๐ฆ๐ง: 3 level nested
โ ๐ญ ๐ง๐ฌ๐ฃ๐-๐ข๐ก๐๐ฌ ๐ง๐๐ฆ๐ง: 5 level nested
โ ๐ญ ๐ง๐ฌ๐ฃ๐-๐ข๐ก๐๐ฌ ๐ง๐๐ฆ๐ง: two identical disj types
โ ๐ญ ๐ง๐ฌ๐ฃ๐-๐ข๐ก๐๐ฌ ๐ง๐๐ฆ๐ง: two differnt disj types
```Automagic registration will examine your environment, try to import various packages, and generally look around. If it doesnโt find anything, it will print the same messages to the console.
Automagic registration uses the [what-the-test](https://github.com/gregros/what-the-test) package, which currently supports:
- Jasmine
- Mocha
- Jest
- Ava
## Manual configuration
If the automagic stuff doesnโt work out, you can always configure `declare-it` manually using the `declare.setup` function:```ts
import {declare} from "declare-it"// Emit to the console:
declare.setup("console")// Don't emit at all:
declare.setup(false)// Use ava:
declare.setup("ava")// Use jest:
declare.setup("jest")
```
# Skipping tests
You can skip compile-time tests in a similar way to how you might skip runtime test.Just prefix the test case declaration with `.skip`:
```ts
declare.it.skip(
"this is a skipped test, so no error", expect => {
expect(type).to_equal(type)
})
```It will make your assertions always pass! It will also register the test as skipped with your test framework.