/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint prettier/prettier: ["warn", { "singleQuote": true, "semi": false, "arrowParens": "avoid", "trailingComma": "es5", "bracketSpacing": false, "endOfLine": "auto", "printWidth": 100 }] */ import {test} from 'vitest' import {expectTypeOf} from '../src/index' test("Check an object's type with `.toEqualTypeOf`", () => { expectTypeOf({a: 1}).toEqualTypeOf<{a: number}>() }) test('`.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](#error-messages))', () => { expectTypeOf({a: 1}).toEqualTypeOf({a: 1}) }) test('`.toEqualTypeOf` succeeds for objects with different values, but the same type', () => { expectTypeOf({a: 1}).toEqualTypeOf({a: 2}) }) test('`.toEqualTypeOf` fails on excess properties', () => { // @ts-expect-error expectTypeOf({a: 1, b: 1}).toEqualTypeOf<{a: number}>() }) test('To allow for extra properties on an object type, use `.toMatchObjectType`. This is a strict check, but only on the subset of keys that are in the expected type', () => { expectTypeOf({a: 1, b: 1}).toMatchObjectType<{a: number}>() }) test('`.toMatchObjectType` can check partial matches on deeply nested objects', () => { const user = { email: 'a@b.com', name: 'John Doe', address: {street: '123 2nd St', city: 'New York', zip: '10001', state: 'NY', country: 'USA'}, } expectTypeOf(user).toMatchObjectType<{name: string; address: {city: string}}>() }) test('To check that a type extends another type, use `.toExtend`', () => { expectTypeOf('some string').toExtend() // @ts-expect-error expectTypeOf({a: 1}).toExtend<{b: number}>() }) test("`.toExtend` can be used with object types, but `.toMatchObjectType` is usually a better choice when dealing with objects, since it's stricter", () => { expectTypeOf({a: 1, b: 2}).toExtend<{a: number}>() // avoid this expectTypeOf({a: 1, b: 2}).toMatchObjectType<{a: number}>() // prefer this }) test('`.toEqualTypeOf`, `.toMatchObjectType`, and `.toExtend` all fail on missing properties', () => { // @ts-expect-error expectTypeOf({a: 1}).toEqualTypeOf<{a: number; b: number}>() // @ts-expect-error expectTypeOf({a: 1}).toMatchObjectType<{a: number; b: number}>() // @ts-expect-error expectTypeOf({a: 1}).toExtend<{a: number; b: number}>() }) test('Another example of the difference between `.toExtend`, `.toMatchObjectType`, and `.toEqualTypeOf`. `.toExtend` can be used for "is-a" relationships', () => { type Fruit = {type: 'Fruit'; edible: boolean} type Apple = {type: 'Fruit'; name: 'Apple'; edible: true} expectTypeOf().toExtend() // @ts-expect-error - the `editable` property isn't an exact match. In `Apple`, it's `true`, which extends `boolean`, but they're not identical. expectTypeOf().toMatchObjectType() // @ts-expect-error - Apple is not an identical type to Fruit, it's a subtype expectTypeOf().toEqualTypeOf() // @ts-expect-error - Apple is a Fruit, but not vice versa expectTypeOf().toExtend() }) test('Assertions can be inverted with `.not`', () => { expectTypeOf({a: 1}).not.toExtend<{b: 1}>() expectTypeOf({a: 1}).not.toMatchObjectType<{b: 1}>() }) test('`.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().toExtend() expectTypeOf().not.toExtend() expectTypeOf().not.toEqualTypeOf() }) test('Catch any/unknown/never types', () => { expectTypeOf().toBeUnknown() expectTypeOf().toBeAny() expectTypeOf().toBeNever() // @ts-expect-error expectTypeOf().toBeNumber() }) test('`.toEqualTypeOf` distinguishes between deeply-nested `any` and `unknown` properties', () => { expectTypeOf<{deeply: {nested: any}}>().not.toEqualTypeOf<{deeply: {nested: unknown}}>() }) test('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() expectTypeOf(1n).toBeBigInt() }) test('`.toBe...` methods allow for types that extend the expected type', () => { expectTypeOf().toBeNumber() expectTypeOf<1>().toBeNumber() expectTypeOf().toBeArray() expectTypeOf().toBeArray() expectTypeOf().toBeString() expectTypeOf<'foo'>().toBeString() expectTypeOf().toBeBoolean() expectTypeOf().toBeBoolean() expectTypeOf().toBeBigInt() expectTypeOf<0n>().toBeBigInt() }) test('`.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() }) test('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() }) test('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() expectTypeOf(1).not.toBeBigInt() }) test('Detect assignability of unioned types', () => { expectTypeOf().toExtend() expectTypeOf().not.toExtend() }) test('Use `.extract` and `.exclude` to narrow down complex union types', () => { type ResponsiveProp = T | T[] | {xs?: T; sm?: T; md?: T} const getResponsiveProp = (_props: T): ResponsiveProp => ({}) type CSSProperties = {margin?: string; padding?: string} const cssProperties: CSSProperties = {margin: '1px', padding: '2px'} expectTypeOf(getResponsiveProp(cssProperties)) .exclude() .exclude<{xs?: unknown}>() .toEqualTypeOf() expectTypeOf(getResponsiveProp(cssProperties)) .extract() .toEqualTypeOf() expectTypeOf(getResponsiveProp(cssProperties)) .extract<{xs?: any}>() .toEqualTypeOf<{xs?: CSSProperties; sm?: CSSProperties; md?: CSSProperties}>() expectTypeOf>().exclude().toHaveProperty('sm') expectTypeOf>().exclude().not.toHaveProperty('xxl') }) test('`.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().extract<{foo: string}>().toBeNever() expectTypeOf().exclude<{name: string}>().toBeNever() }) test('Use `.pick` to pick a set of properties from an object', () => { type Person = {name: string; age: number} expectTypeOf().pick<'name'>().toEqualTypeOf<{name: string}>() }) test('Use `.omit` to remove a set of properties from an object', () => { type Person = {name: string; age: number} expectTypeOf().omit<'name'>().toEqualTypeOf<{age: number}>() }) test('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() }) test('`.toEqualTypeOf` can be used to distinguish between functions', () => { type NoParam = () => void type HasParam = (s: string) => void expectTypeOf().not.toEqualTypeOf() }) test("But often it's preferable to use `.parameters` or `.returns` for more specific function assertions", () => { type NoParam = () => void type HasParam = (s: string) => void expectTypeOf().parameters.toEqualTypeOf<[]>() expectTypeOf().returns.toBeVoid() expectTypeOf().parameters.toEqualTypeOf<[string]>() expectTypeOf().returns.toBeVoid() }) test('Up to ten overloads will produce union types for `.parameters` and `.returns`', () => { type Factorize = { (input: number): number[] (input: bigint): bigint[] } expectTypeOf().parameters.not.toEqualTypeOf<[number]>() expectTypeOf().parameters.toEqualTypeOf<[number] | [bigint]>() expectTypeOf().returns.toEqualTypeOf() expectTypeOf().parameter(0).toEqualTypeOf() }) /** The TypeScript builtins simply choose a single overload (see the [Overloaded functions](#overloaded-functions) section for more information) */ test("Note that these aren't exactly like TypeScript's built-in Parameters<...> and ReturnType<...>", () => { type Factorize = { (input: number): number[] (input: bigint): bigint[] } // overload using `number` is ignored! expectTypeOf>().toEqualTypeOf<[bigint]>() expectTypeOf>().toEqualTypeOf() }) test('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]>() }) test('`.toBeCallableWith` allows for overloads. You can also use it to narrow down the return type for given input parameters.', () => { type Factorize = { (input: number): number[] (input: bigint): bigint[] } expectTypeOf().toBeCallableWith(6) expectTypeOf().toBeCallableWith(6n) }) test('`.toBeCallableWith` returns a type that can be used to narrow down the return type for given input parameters.', () => { type Factorize = { (input: number): number[] (input: bigint): bigint[] } expectTypeOf().toBeCallableWith(6).returns.toEqualTypeOf() expectTypeOf().toBeCallableWith(6n).returns.toEqualTypeOf() }) test('`.toBeCallableWith` can be used to narrow down the parameters of a function', () => { type Delete = { (path: string): void (paths: string[], options?: {force: boolean}): void } expectTypeOf().toBeCallableWith('abc').parameters.toEqualTypeOf<[string]>() expectTypeOf() .toBeCallableWith(['abc', 'def'], {force: true}) .parameters.toEqualTypeOf<[string[], {force: boolean}?]>() expectTypeOf().toBeCallableWith('abc').parameter(0).toBeString() expectTypeOf().toBeCallableWith('abc').parameter(1).toBeUndefined() expectTypeOf() .toBeCallableWith(['abc', 'def', 'ghi']) .parameter(0) .toEqualTypeOf() expectTypeOf() .toBeCallableWith(['abc', 'def', 'ghi']) .parameter(1) .toEqualTypeOf<{force: boolean} | undefined>() }) test("You can't use `.toBeCallableWith` with `.not` - you need to use ts-expect-error:", () => { const f = (a: number) => [a, a] // @ts-expect-error expectTypeOf(f).toBeCallableWith('foo') }) /** * This can be useful for generic functions or complex types which you can't access via `.toBeCallableWith`, `.toHaveProperty` etc. * The callback function isn't called at runtime, which can make this a useful way to get complex inferred types without worrying about running code. */ test('Use `.map` to transform types', () => { const capitalize = (input: S) => (input.slice(0, 1).toUpperCase() + input.slice(1)) as Capitalize expectTypeOf(capitalize) .map(fn => fn('hello world')) .toEqualTypeOf<'Hello world'>() }) test('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() const isBigInt = (value: any): value is bigint => typeof value === 'bigint' expectTypeOf(isBigInt).guards.toBeBigInt() }) test('Assert on constructor parameters', () => { expectTypeOf(Date).toBeConstructibleWith('1970') expectTypeOf(Date).toBeConstructibleWith(0) expectTypeOf(Date).toBeConstructibleWith(new Date()) expectTypeOf(Date).toBeConstructibleWith() expectTypeOf(Date).constructorParameters.toEqualTypeOf< | [] | [value: string | number] | [value: string | number | Date] | [ year: number, monthIndex: number, date?: number | undefined, hours?: number | undefined, minutes?: number | undefined, seconds?: number | undefined, ms?: number | undefined, ] >() }) test('Constructor overloads', () => { class DBConnection { constructor() constructor(connectionString: string) constructor(options: {host: string; port: number}) constructor(..._: unknown[]) {} } expectTypeOf(DBConnection).toBeConstructibleWith() expectTypeOf(DBConnection).toBeConstructibleWith('localhost') expectTypeOf(DBConnection).toBeConstructibleWith({host: 'localhost', port: 1234}) // @ts-expect-error - as when calling `new DBConnection(...)` you can't actually use the `(...args: unknown[])` overlaod, it's purely for the implementation. expectTypeOf(DBConnection).toBeConstructibleWith(1, 2) }) test('Check function `this` parameters', () => { function greet(this: {name: string}, message: string) { return `Hello ${this.name}, here's your message: ${message}` } expectTypeOf(greet).thisParameter.toEqualTypeOf<{name: string}>() }) test('Distinguish between functions with different `this` parameters', () => { function greetFormal(this: {title: string; name: string}, message: string) { return `Dear ${this.title} ${this.name}, here's your message: ${message}` } function greetCasual(this: {name: string}, message: string) { return `Hi ${this.name}, here's your message: ${message}` } expectTypeOf(greetFormal).not.toEqualTypeOf(greetCasual) }) test('Class instance types', () => { expectTypeOf(Date).instance.toHaveProperty('toISOString') }) test('Promise resolution types can be checked with `.resolves`', () => { const asyncFunc = async () => 123 expectTypeOf(asyncFunc).returns.resolves.toBeNumber() }) test('Array items can be checked with `.items`', () => { expectTypeOf([1, 2, 3]).items.toBeNumber() expectTypeOf([1, 2, 3]).items.not.toBeString() }) test('You can also compare arrays directly', () => { expectTypeOf().not.toEqualTypeOf() }) test('Check that functions never return', () => { const thrower = () => { throw new Error('oh no') } expectTypeOf(thrower).returns.toBeNever() }) test('Generics can be used rather than references', () => { expectTypeOf<{a: string}>().not.toEqualTypeOf<{a: number}>() }) test('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: {}}>() }) test('Detect the difference between regular and `readonly` properties', () => { type A1 = {readonly a: string; b: string} type E1 = {a: string; b: string} expectTypeOf().toExtend() expectTypeOf().not.toEqualTypeOf() type A2 = {a: string; b: {readonly c: string}} type E2 = {a: string; b: {c: string}} expectTypeOf().toExtend() expectTypeOf().not.toEqualTypeOf() }) test('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().not.toEqualTypeOf() class C { value: number constructor(c: 1) { this.value = c } } expectTypeOf().toEqualTypeOf() }) test('Known limitation: Intersection types can cause issues with `toEqualTypeOf`', () => { // @ts-expect-error the following line doesn't compile, even though the types are arguably the same. // See https://github.com/mmkal/expect-type/pull/21 expectTypeOf<{a: 1} & {b: 2}>().toEqualTypeOf<{a: 1; b: 2}>() }) test('To workaround for simple cases, you can use a mapped type', () => { type Simplify = {[K in keyof T]: T[K]} expectTypeOf>().toEqualTypeOf<{a: 1; b: 2}>() }) test("But this won't work if the nesting is deeper in the type. For these situations, you can use the `.branded` helper. Note that this comes at a performance cost, and can cause the compiler to 'give up' if used with excessively deep types, so use sparingly. This helper is under `.branded` because it deeply transforms the Actual and Expected types into a pseudo-AST", () => { // @ts-expect-error expectTypeOf<{a: {b: 1} & {c: 1}}>().toEqualTypeOf<{a: {b: 1; c: 1}}>() expectTypeOf<{a: {b: 1} & {c: 1}}>().branded.toEqualTypeOf<{a: {b: 1; c: 1}}>() }) test('Be careful with `.branded` for very deep or complex types, though. If possible you should find a way to simplify your test to avoid needing to use it', () => { // This *should* result in an error, but the "branding" mechanism produces too large a type and TypeScript just gives up! https://github.com/microsoft/TypeScript/issues/50670 expectTypeOf<() => () => () => () => 1>().branded.toEqualTypeOf<() => () => () => () => 2>() // @ts-expect-error the non-branded implementation catches the error as expected. expectTypeOf<() => () => () => () => 1>().toEqualTypeOf<() => () => () => () => 2>() }) test("So, if you have an extremely deep type that ALSO has an intersection in it, you're out of luck and this library won't be able to test your type properly", () => { // @ts-expect-error this fails, but it should succeed. expectTypeOf<() => () => () => () => {a: 1} & {b: 2}>().toEqualTypeOf< () => () => () => () => {a: 1; b: 2} >() // this succeeds, but it should fail. expectTypeOf<() => () => () => () => {a: 1} & {b: 2}>().branded.toEqualTypeOf< () => () => () => () => {a: 1; c: 2} >() }) test('Another limitation: passing `this` references to `expectTypeOf` results in errors.', () => { class B { b = 'b' foo() { // @ts-expect-error expectTypeOf(this).toEqualTypeOf(this) } } // Instead of the above, try something like this: expectTypeOf(B).instance.toEqualTypeOf<{b: string; foo: () => void}>() })