Skip to content

Releases: mmkal/expect-type

v1.1.0

09 Oct 19:03
Compare
Choose a tag to compare

What's Changed

Full Changelog: v1.0.0...v1.1.0

v1.0.0

03 Oct 18:34
Compare
Choose a tag to compare

v1! 🎉🎉🎉

X (formerly Twitter) Follow

After many years being commitment-phobic, expect-type is now in v1.

This release does not add any user facing features on top of v0.20.0 or v1.0.0-rc.0. It's just "making it official". For anyone new to the project, or coming here from vitest or viteconf (👋 ), the usage docs from the readme are pasted below.

For anyone on an old-ish v0 version, here are links to the non-trivial changes that have gone in since v0.15.0:

  • v0.20.0: Function overloads support (proper support, beyond the default typescript functionality which eliminates all but one overloads by default)
  • v0.19.0: Beefed up JSDocs thanks to @aryaemami59
  • v0.18.0: .pick and .omit thanks to @aryaemami59
  • v0.17.0: massively improved error messages, so (in most cases) when an assertion fails you can see what's wrong, not just that something is wrong
  • v0.16.0: default to internal typescript implementation of type-identicalness. Introduce the .branded helper for the old behaviour. Also support function this parameters - thank to @trevorade and @papb

Full usage docs below, for newbies (head to the readme to keep up to date):

docs from readme

Installation and usage

npm install expect-type --save-dev
import {expectTypeOf} from 'expect-type'

Documentation

The expectTypeOf method takes a single argument or a generic type parameter. Neither it nor the functions chained off its return value have any meaningful runtime behaviour. The assertions you write will be compile-time errors if they don't hold true.

Features

Check an object's type with .toEqualTypeOf:

expectTypeOf({a: 1}).toEqualTypeOf<{a: number}>()

.toEqualTypeOf can check that two concrete objects have equivalent types (note: when these assertions fail, the error messages can be less informative vs the generic type argument syntax above - see error messages docs):

expectTypeOf({a: 1}).toEqualTypeOf({a: 1})

.toEqualTypeOf succeeds for objects with different values, but the same type:

expectTypeOf({a: 1}).toEqualTypeOf({a: 2})

.toEqualTypeOf fails on excess properties:

// @ts-expect-error
expectTypeOf({a: 1, b: 1}).toEqualTypeOf<{a: number}>()

To allow for extra properties, use .toMatchTypeOf. This is roughly equivalent to an extends constraint in a function type argument.:

expectTypeOf({a: 1, b: 1}).toMatchTypeOf<{a: number}>()

.toEqualTypeOf and .toMatchTypeOf both fail on missing properties:

// @ts-expect-error
expectTypeOf({a: 1}).toEqualTypeOf<{a: number; b: number}>()
// @ts-expect-error
expectTypeOf({a: 1}).toMatchTypeOf<{a: number; b: number}>()

Another example of the difference between .toMatchTypeOf and .toEqualTypeOf, using generics. .toMatchTypeOf can be used for "is-a" relationships:

type Fruit = {type: 'Fruit'; edible: boolean}
type Apple = {type: 'Fruit'; name: 'Apple'; edible: true}

expectTypeOf<Apple>().toMatchTypeOf<Fruit>()

// @ts-expect-error
expectTypeOf<Fruit>().toMatchTypeOf<Apple>()

// @ts-expect-error
expectTypeOf<Apple>().toEqualTypeOf<Fruit>()

Assertions can be inverted with .not:

expectTypeOf({a: 1}).not.toMatchTypeOf({b: 1})

.not can be easier than relying on // @ts-expect-error:

type Fruit = {type: 'Fruit'; edible: boolean}
type Apple = {type: 'Fruit'; name: 'Apple'; edible: true}

expectTypeOf<Apple>().toMatchTypeOf<Fruit>()

expectTypeOf<Fruit>().not.toMatchTypeOf<Apple>()
expectTypeOf<Apple>().not.toEqualTypeOf<Fruit>()

Catch any/unknown/never types:

expectTypeOf<unknown>().toBeUnknown()
expectTypeOf<any>().toBeAny()
expectTypeOf<never>().toBeNever()

// @ts-expect-error
expectTypeOf<never>().toBeNumber()

.toEqualTypeOf distinguishes between deeply-nested any and unknown properties:

expectTypeOf<{deeply: {nested: any}}>().not.toEqualTypeOf<{deeply: {nested: unknown}}>()

You can test for basic JavaScript types:

expectTypeOf(() => 1).toBeFunction()
expectTypeOf({}).toBeObject()
expectTypeOf([]).toBeArray()
expectTypeOf('').toBeString()
expectTypeOf(1).toBeNumber()
expectTypeOf(true).toBeBoolean()
expectTypeOf(() => {}).returns.toBeVoid()
expectTypeOf(Promise.resolve(123)).resolves.toBeNumber()
expectTypeOf(Symbol(1)).toBeSymbol()

.toBe... methods allow for types that extend the expected type:

expectTypeOf<number>().toBeNumber()
expectTypeOf<1>().toBeNumber()

expectTypeOf<any[]>().toBeArray()
expectTypeOf<number[]>().toBeArray()

expectTypeOf<string>().toBeString()
expectTypeOf<'foo'>().toBeString()

expectTypeOf<boolean>().toBeBoolean()
expectTypeOf<true>().toBeBoolean()

.toBe... methods protect against any:

const goodIntParser = (s: string) => Number.parseInt(s, 10)
const badIntParser = (s: string) => JSON.parse(s) // uh-oh - works at runtime if the input is a number, but return 'any'

expectTypeOf(goodIntParser).returns.toBeNumber()
// @ts-expect-error - if you write a test like this, `.toBeNumber()` will let you know your implementation returns `any`.
expectTypeOf(badIntParser).returns.toBeNumber()

Nullable types:

expectTypeOf(undefined).toBeUndefined()
expectTypeOf(undefined).toBeNullable()
expectTypeOf(undefined).not.toBeNull()

expectTypeOf(null).toBeNull()
expectTypeOf(null).toBeNullable()
expectTypeOf(null).not.toBeUndefined()

expectTypeOf<1 | undefined>().toBeNullable()
expectTypeOf<1 | null>().toBeNullable()
expectTypeOf<1 | undefined | null>().toBeNullable()

More .not examples:

expectTypeOf(1).not.toBeUnknown()
expectTypeOf(1).not.toBeAny()
expectTypeOf(1).not.toBeNever()
expectTypeOf(1).not.toBeNull()
expectTypeOf(1).not.toBeUndefined()
expectTypeOf(1).not.toBeNullable()

Detect assignability of unioned types:

expectTypeOf<number>().toMatchTypeOf<string | number>()
expectTypeOf<string | number>().not.toMatchTypeOf<number>()

Use .extract and .exclude to narrow down complex union types:

type ResponsiveProp<T> = T | T[] | {xs?: T; sm?: T; md?: T}
const getResponsiveProp = <T>(_props: T): ResponsiveProp<T> => ({})
type CSSProperties = {margin?: string; padding?: string}

const cssProperties: CSSProperties = {margin: '1px', padding: '2px'}

expectTypeOf(getResponsiveProp(cssProperties))
  .exclude<unknown[]>()
  .exclude<{xs?: unknown}>()
  .toEqualTypeOf<CSSProperties>()

expectTypeOf(getResponsiveProp(cssProperties))
  .extract<unknown[]>()
  .toEqualTypeOf<CSSProperties[]>()

expectTypeOf(getResponsiveProp(cssProperties))
  .extract<{xs?: any}>()
  .toEqualTypeOf<{xs?: CSSProperties; sm?: CSSProperties; md?: CSSProperties}>()

expectTypeOf<ResponsiveProp<number>>().exclude<number | number[]>().toHaveProperty('sm')
expectTypeOf<ResponsiveProp<number>>().exclude<number | number[]>().not.toHaveProperty('xxl')

.extract and .exclude return never if no types remain after exclusion:

type Person = {name: string; age: number}
type Customer = Person & {customerId: string}
type Employee = Person & {employeeId: string}

expectTypeOf<Customer | Employee>().extract<{foo: string}>().toBeNever()
expectTypeOf<Customer | Employee>().exclude<{name: string}>().toBeNever()

Use .pick to pick a set of properties from an object:

type Person = {name: string; age: number}

expectTypeOf<Person>().pick<'name'>().toEqualTypeOf<{name: string}>()

Use .omit to remove a set of properties from an object:

type Person = {name: string; age: number}

expectTypeOf<Person>().omit<'name'>().toEqualTypeOf<{age: number}>()

Make assertions about object properties:

const obj = {a: 1, b: ''}

// check that properties exist (or don't) with `.toHaveProperty`
expectTypeOf(obj).toHaveProperty('a')
expectTypeOf(obj).not.toHaveProperty('c')

// check types of properties
expectTypeOf(obj).toHaveProperty('a').toBeNumber()
expectTypeOf(obj).toHaveProperty('b').toBeString()
expectTypeOf(obj).toHaveProperty('a').not.toBeString()

.toEqualTypeOf can be used to distinguish between functions:

type NoParam = () => void
type HasParam = (s: string) => void

expectTypeOf<NoParam>().not.toEqualTypeOf<HasParam>()

But often it's preferable to use .parameters or .returns for more specific function assertions:

type NoParam = () => void
type HasParam = (s: string) => void

expectTypeOf<NoParam>().parameters.toEqualTypeOf<[]>()
expectTypeOf<NoParam>().returns.toBeVoid()

expectTypeOf<HasParam>().parameters.toEqualTypeOf<[string]>()
expectTypeOf<HasParam>().returns.toBeVoid()

Up to ten overloads will produce union types for .parameters and .returns:

type Factorize = {
  (input: number): number[]
  (input: bigint): bigint[]
}

expectTypeOf<Factorize>().parameters.toEqualTypeOf<[number] | [bigint]>()
expectTypeOf<Factorize>().returns.toEqualTypeOf<number[] | bigint[]>()

expectTypeOf<Factorize>().parameter(0).toEqualTypeOf<number | bigint>()

Note that these aren't exactly like TypeScr...

Read more

v1.0.0-rc.0

10 Sep 01:50
Compare
Choose a tag to compare
v1.0.0-rc.0 Pre-release
Pre-release

1.0.0 release candidate

No changes other than dev dependency updates since v0.20.0: https://github.com/mmkal/expect-type/releases/tag/v0.20.0

The intent is to publish a 1.0.0 as-is, since this is used in vitest already.

v0.20.0

20 Aug 16:20
Compare
Choose a tag to compare

Breaking changes

  • improve overloads support, attempt 2 by @mmkal in #83

This change updates how overloaded functions are treated. Now, .parameters gives you a union of the parameter-tuples that a function can take. For example, given the following type:

type Factorize = {
  (input: number): number[]
  (input: bigint): bigint[]
}

Behvaiour before:

expectTypeOf<Factorize>().parameters.toEqualTypeOf<[bigint]>()

Behaviour now:

expectTypeOf<Factorize>().parameters.toEqualTypeOf<[number] | [bigint]>()

There were similar changes for .returns, .parameter(...), and .toBeCallableWith. Also, overloaded functions are now differentiated properly when using .branded.toEqualTypeOf (this was a bug that it seems nobody found).

See #83 for more details or look at the updated docs (including a new section called "Overloaded functions", which has more info on how this behaviour differs for TypeScript versions before 5.3).

What's Changed

Full Changelog: v0.19.0...v0.20.0

v0.20.0-0

13 Aug 18:18
Compare
Choose a tag to compare
v0.20.0-0 Pre-release
Pre-release

Breaking changes

  • improve overloads support, attempt 2 by @mmkal in #83

This change updates how overloaded functions are treated. Now, .parameters gives you a union of the parameter-tuples that a function can take. For example, given the following type:

type Factorize = {
  (input: number): number[]
  (input: bigint): bigint[]
}

Behvaiour before:

expectTypeOf<Factorize>().parameters.toEqualTypeOf<[bigint]>()

Behaviour now:

expectTypeOf<Factorize>().parameters.toEqualTypeOf<[number] | [bigint]>()

There were similar changes for .returns, .parameter(...), and .toBeCallableWith. Also, overloaded functions are now differentiated properly when using .branded.toEqualTypeOf (this was a bug that it seems nobody found).

See #83 for more details or look at the updated docs (including a new section called "Overloaded functions", which has more info on how this behaviour differs for TypeScript versions before 5.3.

What's Changed

New Contributors

Full Changelog: v0.17.3...v0.20.0-0

0.19.0

21 Mar 21:00
Compare
Choose a tag to compare

What's Changed

Full Changelog: 0.18.0...0.19.0

0.18.0

26 Feb 21:15
Compare
Choose a tag to compare

What's Changed

New Contributors

Full Changelog: v0.17.3...0.18.0

v0.17.3

03 Oct 20:57
Compare
Choose a tag to compare

v0.17.3-0...v0.17.3

v0.17.2

03 Oct 13:59
Compare
Choose a tag to compare

Diff(truncated - scroll right!):

test('toEqualTypeOf with tuples', () => {
  const assertion = `expectTypeOf<[[number], [1], []]>().toEqualTypeOf<[[number], [2], []]>()`
  expect(tsErrors(assertion)).toMatchInlineSnapshot(`
-    "test/test.ts:999:999 - error TS2344: Type '[[number], [2], []]' does not satisfy the constraint '{ [x: number]: { [x: number]: number; [iterator]: (() => IterableIterator<1>) | (() => IterableIterator<number>) | (() => IterableIterator<never>); [unscopables]: (() => { copyWithin: boolean; entries: boolean; fill: boolean; find: boolean; findIndex: boolean; keys: boolean; values: boolean; }) | (() => { copyWithin: boolean; entries: boolean; fill: boolean; find: boolean; findIndex: boolean; keys: boolean; values: boolean; }) | (() => { copyWithin: boolean; entries: boolean; fill: boolean; find: boolean; findIndex: boolean; keys: boolean; values: boolean; }); length: 0 | 1; toString:  ... truncated!!!!'.
-      Types of property 'sort' are incompatible.
-        Type '(compareFn?: ((a: [] | [number] | [2], b: [] | [number] | [2]) => number) | undefined) => [[number], [2], []]' is not assignable to type '\\"Expected: function, Actual: function\\"'.
+    "test/test.ts:999:999 - error TS2344: Type '[[number], [2], []]' does not satisfy the constraint '{ 0: { 0: number; }; 1: { 0: \\"Expected: literal number: 2, Actual: literal number: 1\\"; }; 2: {}; }'.
+      The types of '1[0]' are incompatible between these types.
+        Type '2' is not assignable to type '\\"Expected: literal number: 2, Actual: literal number: 1\\"'.
    999 expectTypeOf<[[number], [1], []]>().toEqualTypeOf<[[number], [2], []]>()
                                                          ~~~~~~~~~~~~~~~~~~~"
  `)
})

v0.17.1...v0.17.2

v0.17.1

03 Oct 02:08
Compare
Choose a tag to compare
  • disallow .not and .branded together cf38918

(this was actually documented in the v0.17.0 release but really it was only pushed here)

v0.17.0...v0.17.1