Releases: mmkal/expect-type
v1.1.0
v1.0.0
v1! 🎉🎉🎉
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 functionthis
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...
v1.0.0-rc.0
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
Breaking changes
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
- Fix rendering issue in readme by @mrazauskas in #69
- Fix minor issues in docs by @aryaemami59 in #91
- create utils file by @mmkal in #93
- branding.ts and messages.ts by @mmkal in #95
- improve overloads support, attempt 2 by @mmkal in #83
- Extends: explain myself 1e37116
- Mark internal APIs with
@internal
JSDoc tag (#104) 4c40b07 - Re-export everything in
overloads.ts
file (#107) 5ee0181 - JSDoc improvements (#100) 0bbeffa
Full Changelog: v0.19.0...v0.20.0
v0.20.0-0
Breaking changes
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
- Add
.pick
and.omit
by @aryaemami59 in #51 - Fix
.omit()
to work similarly toOmit
by @aryaemami59 in #54 - Add JSDocs to everything by @aryaemami59 in #56
- Test against different versions of TypeScript during CI by @aryaemami59 in #62
- Fix rendering issue in readme by @mrazauskas in #69
- Update LICENSE file to properly include copyright information by @trevorade in #72
- Test against TypeScript version 5.5 in CI by @aryaemami59 in #86
- Fix minor issues in docs by @aryaemami59 in #91
- create utils file by @mmkal in #93
- branding.ts and messages.ts by @mmkal in #95
- improve overloads support, attempt 2 by @mmkal in #83
New Contributors
- @renovate made their first contribution in #1
- @aryaemami59 made their first contribution in #51
- @mrazauskas made their first contribution in #69
- @github-actions made their first contribution in #88
Full Changelog: v0.17.3...v0.20.0-0
0.19.0
What's Changed
- Fix
.omit()
to work similarly toOmit
by @aryaemami59 in #54 - Add JSDocs to everything by @aryaemami59 in #56
- Remove
test
import inREADME.md
by @aryaemami59 in #65
Full Changelog: 0.18.0...0.19.0
0.18.0
What's Changed
- Add
.pick
and.omit
by @aryaemami59 in #51
New Contributors
- @aryaemami59 made their first contribution in #51
Full Changelog: v0.17.3...0.18.0
v0.17.3
- docs: why-is-my-assertion-failing 907b8aa
- I think the previous build was out of date somehow too, so see https://github.com/mmkal/expect-type/releases/v0.17.2
v0.17.2
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
- disallow
.not
and.branded
together cf38918
(this was actually documented in the v0.17.0 release but really it was only pushed here)