expect-type
    TypeScript icon, indicating that this package has built-in type declarations

    0.13.0 • Public • Published

    expect-type

    Compile-time tests for types. Useful to make sure types don't regress into being overly-permissive as changes go in over time.

    Node CI codecov npm version

    Similar to Jest's expect, but with type-awareness. Gives you access to a number of type-matchers that let you make assertions about the form of a reference or generic type parameter.

    import {foo, bar} from '../foo'
    import {expectTypeOf} from 'expect-type'
    
    test('foo types', () => {
      // make sure `foo` has type {a: number}
      expectTypeOf(foo).toMatchTypeOf<{a: number}>()
    
      // make sure `bar` is a function taking a string:
      expectTypeOf(bar).parameter(0).toBeString()
      expectTypeOf(bar).returns.not.toBeAny()
    })

    It can be used in your existing test files - or any other type-checked file you'd like - it's built into existing tooling with no dependencies. No extra build step, cli tool, IDE extension, or lint plugin is needed. Just import the function and start writing tests. Failures will be at compile time - they'll appear in your IDE and when you run tsc.

    See below for lots more examples.

    Contents

    Installation and usage

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

    Documentation

    The expectTypeOf method takes a single argument, or a generic 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:

    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 extra properties:

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

    To allow for extra properties, use .toMatchTypeOf. This checks that an object "matches" a type. This is similar to jest's .toMatchObject:

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

    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}}>()

    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()

    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()

    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()

    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()

    More examples of ways to work with functions - parameters using .parameter(n) or .parameters, and return values using .returns:

    const f = (a: number) => [a, a]
    
    expectTypeOf(f).toBeFunction()
    
    expectTypeOf(f).toBeCallableWith(1)
    expectTypeOf(f).not.toBeAny()
    expectTypeOf(f).returns.not.toBeAny()
    expectTypeOf(f).returns.toEqualTypeOf([1, 2])
    expectTypeOf(f).returns.toEqualTypeOf([1, 2, 3])
    expectTypeOf(f).parameter(0).not.toEqualTypeOf('1')
    expectTypeOf(f).parameter(0).toEqualTypeOf(1)
    expectTypeOf(1).parameter(0).toBeNever()
    
    const twoArgFunc = (a: number, b: string) => ({a, b})
    
    expectTypeOf(twoArgFunc).parameters.toEqualTypeOf<[number, string]>()

    You can also check type guards & type assertions:

    const assertNumber = (v: any): asserts v is number => {
      if (typeof v !== 'number') {
        throw new TypeError('Nope !')
      }
    }
    expectTypeOf(assertNumber).asserts.toBeNumber()
    
    const isString = (v: any): v is string => typeof v === 'string'
    expectTypeOf(isString).guards.toBeString()

    Assert on constructor parameters:

    expectTypeOf(Date).toBeConstructibleWith('1970')
    expectTypeOf(Date).toBeConstructibleWith(0)
    expectTypeOf(Date).toBeConstructibleWith(new Date())
    expectTypeOf(Date).toBeConstructibleWith()
    
    expectTypeOf(Date).constructorParameters.toEqualTypeOf<[] | [string | number | Date]>()

    Class instance types:

    expectTypeOf(Date).instance.toHaveProperty('toISOString')

    Promise resolution types can be checked with .resolves:

    const asyncFunc = async () => 123
    
    expectTypeOf(asyncFunc).returns.resolves.toBeNumber()

    Array items can be checked with .items:

    expectTypeOf([1, 2, 3]).items.toBeNumber()
    expectTypeOf([1, 2, 3]).items.not.toBeString()

    Check that functions never return:

    const thrower = () => {
      throw Error('oh no')
    }
    
    expectTypeOf(thrower).returns.toBeNever()

    Generics can be used rather than references:

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

    Distinguish between missing/null/optional properties:

    expectTypeOf<{a?: number}>().not.toEqualTypeOf<{}>()
    expectTypeOf<{a?: number}>().not.toEqualTypeOf<{a: number}>()
    expectTypeOf<{a?: number}>().not.toEqualTypeOf<{a: number | undefined}>()
    expectTypeOf<{a?: number | null}>().not.toEqualTypeOf<{a: number | null}>()
    expectTypeOf<{a: {b?: number}}>().not.toEqualTypeOf<{a: {}}>()

    Detect the difference between regular and readonly properties:

    type A1 = {readonly a: string; b: string}
    type E1 = {a: string; b: string}
    
    expectTypeOf<A1>().toMatchTypeOf<E1>()
    expectTypeOf<A1>().not.toEqualTypeOf<E1>()
    
    type A2 = {a: string; b: {readonly c: string}}
    type E2 = {a: string; b: {c: string}}
    
    expectTypeOf<A2>().toMatchTypeOf<E2>()
    expectTypeOf<A2>().not.toEqualTypeOf<E2>()

    Distinguish between classes with different constructors:

    class A {
      value: number
      constructor(a: 1) {
        this.value = a
      }
    }
    class B {
      value: number
      constructor(b: 2) {
        this.value = b
      }
    }
    
    expectTypeOf<typeof A>().not.toEqualTypeOf<typeof B>()
    
    class C {
      value: number
      constructor(c: 1) {
        this.value = c
      }
    }
    
    expectTypeOf<typeof A>().toEqualTypeOf<typeof C>()

    Within test frameworks

    Jest & eslint-plugin-jest

    If you're using Jest along with eslint-plugin-jest, you will get warnings from the jest/expect-expect rule, complaining that "Test has no assertions" for tests that only use expectTypeOf().

    To remove this warning, configure the ESlint rule to consider expectTypeOf as an assertion:

    "rules": {
      // ...
      "jest/expect-expect": [
        "warn",
        {
          "assertFunctionNames": [
            "expect", "expectTypeOf"
          ]
        }
      ],
      // ...
    }

    Similar projects

    Other projects with similar goals:

    • tsd is a CLI that runs the TypeScript type checker over assertions
    • ts-expect exports several generic helper types to perform type assertions
    • dtslint does type checks via comment directives and tslint
    • type-plus comes with various type and runtime TypeScript assertions
    • static-type-assert type assertion functions

    Comparison

    The key differences in this project are:

    • a fluent, jest-inspired API, making the difference between actual and expected clear. This is helpful with complex types and assertions.
    • inverting assertions intuitively and easily via expectTypeOf(...).not
    • first-class support for:
      • any (as well as unknown and never) (see issues outstanding at time of writing in tsd for never and any).
        • This can be especially useful in combination with not, to protect against functions returning too-permissive types. For example, const parseFile = (filename: string) => JSON.parse(readFileSync(filename).toString()) returns any, which could lead to errors. After giving it a proper return-type, you can add a test for this with expect(parseFile).returns.not.toBeAny()
      • object properties
      • function parameters
      • function return values
      • constructor parameters
      • class instances
      • array item values
      • nullable types
    • assertions on types "matching" rather than exact type equality, for "is-a" relationships e.g. expectTypeOf(square).toMatchTypeOf<Shape>()
    • built into existing tooling. No extra build step, cli tool, IDE extension, or lint plugin is needed. Just import the function and start writing tests. Failures will be at compile time - they'll appear in your IDE and when you run tsc.
    • small implementation with no dependencies. <200 lines of code - take a look! (tsd, for comparison, is 2.6MB because it ships a patched version of typescript).

    Install

    npm i expect-type

    DownloadsWeekly Downloads

    19,114

    Version

    0.13.0

    License

    Apache-2.0

    Unpacked Size

    59.8 kB

    Total Files

    10

    Last publish

    Collaborators

    • mmkale