From 3daba59b63083fd879c5651f159d7af5b8e39481 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Sun, 17 Mar 2024 09:29:15 -0500 Subject: [PATCH] Add JSDocs to everything (#56) ## This PR: - [X] Adds inline documentation to everything inside of the library in the form of JSDocs. --- src/index.ts | 859 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 831 insertions(+), 28 deletions(-) diff --git a/src/index.ts b/src/index.ts index 220ff8f..79230b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,17 +1,56 @@ +/** + * Negates a boolean type. + */ export type Not = T extends true ? false : true + +/** + * Returns `true` if at least one of the types in the + * {@linkcode Types} array is `true`, otherwise returns `false`. + */ export type Or = Types[number] extends false ? false : true + +/** + * Checks if all the boolean types in the {@linkcode Types} array are `true`. + */ export type And = Types[number] extends true ? true : false + +/** + * Represents an equality type that returns {@linkcode Right} if + * {@linkcode Left} is `true`, + * otherwise returns the negation of {@linkcode Right}. + */ export type Eq = Left extends true ? Right : Not + +/** + * Represents the exclusive OR operation on a tuple of boolean types. + * Returns `true` if exactly one of the boolean types is `true`, + * otherwise returns `false`. + */ export type Xor = Not> const secret = Symbol('secret') type Secret = typeof secret +/** + * Checks if the given type is `never`. + */ export type IsNever = [T] extends [never] ? true : false +/** + * Checks if the given type is `any`. + */ export type IsAny = [T] extends [Secret] ? Not> : false +/** + * Determines if the given type is `unknown`. + */ export type IsUnknown = [unknown] extends [T] ? Not> : false +/** + * Determines if a type is either `never` or `any`. + */ export type IsNeverOrAny = Or<[IsNever, IsAny]> +/** + * Determines the printable type representation for a given type. + */ export type PrintType = IsUnknown extends true ? 'unknown' : IsNever extends true @@ -38,11 +77,18 @@ export type PrintType = IsUnknown extends true ? 'function' : '...' -/** Subjective "useful" keys from a type. For objects it's just `keyof` but for tuples/arrays it's the number keys +/** + * Subjective "useful" keys from a type. For objects it's just `keyof` but for + * tuples/arrays it's the number keys. + * * @example + * ```ts * UsefulKeys<{a: 1; b: 2}> // 'a' | 'b' + * * UsefulKeys<['a', 'b']> // '0' | '1' + * * UsefulKeys // number + * ``` */ export type UsefulKeys = T extends any[] ? { @@ -67,15 +113,20 @@ export type MismatchInfo = And<[Extends, '.. : `Expected: ${PrintType}, Actual: ${PrintType>}` /** - * Recursively walk a type and replace it with a branded type related to the original. This is useful for - * equality-checking stricter than `A extends B ? B extends A ? true : false : false`, because it detects - * the difference between a few edge-case types that vanilla typescript doesn't by default: + * Represents a deeply branded type. + * + * Recursively walk a type and replace it with a branded type related to the + * original. This is useful for equality-checking stricter than + * `A extends B ? B extends A ? true : false : false`, because it detects the + * difference between a few edge-case types that vanilla typescript + * doesn't by default: * - `any` vs `unknown` * - `{ readonly a: string }` vs `{ a: string }` * - `{ a?: string }` vs `{ a: string | undefined }` * - * Note: not very performant for complex types - this should only be used when you know you need it. If doing - * an equality check, it's almost always better to use `StrictEqualUsingTSInternalIdenticalToOperator`. + * __Note__: not very performant for complex types - this should only be used + * when you know you need it. If doing an equality check, it's almost always + * better to use {@linkcode StrictEqualUsingTSInternalIdenticalToOperator}. */ export type DeepBrand = IsNever extends true ? {type: 'never'} @@ -116,16 +167,25 @@ export type DeepBrand = IsNever extends true constructorParams: DeepBrand> } +/** + * Extracts the keys from a type that are required (not optional). + */ export type RequiredKeys = Extract< { [K in keyof T]-?: {} extends Pick ? never : K }[keyof T], keyof T > +/** + * Gets the keys of an object type that are optional. + */ export type OptionalKeys = Exclude> // adapted from some answers to https://github.com/type-challenges/type-challenges/issues?q=label%3A5+label%3Aanswer // prettier-ignore +/** + * Extracts the keys from a type that are not readonly. + */ export type ReadonlyKeys = Extract<{ [K in keyof T]-?: ReadonlyEquivalent< {[_K in K]: T[K]}, @@ -134,17 +194,27 @@ export type ReadonlyKeys = Extract<{ }[keyof T], keyof T>; // prettier-ignore +/** + * Determines if two types, are equivalent in a `readonly` manner. + */ type ReadonlyEquivalent = Extends< (() => T extends X ? true : false), (() => T extends Y ? true : false) > -/** Returns true if `L extends R`. Explicitly checks for `never` since that can give unexpected results. */ +/** + * Checks if one type extends another. + */ export type Extends = IsNever extends true ? IsNever : [L] extends [R] ? true : false export type ExtendsUsingBranding = Extends, DeepBrand> export type ExtendsExcludingAnyOrNever = IsAny extends true ? IsAny : Extends -// much history: https://github.com/microsoft/TypeScript/issues/55188#issuecomment-1656328122 +/** + * Checks if two types are strictly equal using + * the TypeScript internal identical-to operator. + * + * @see {@link https://github.com/microsoft/TypeScript/issues/55188#issuecomment-1656328122 much history} + */ type StrictEqualUsingTSInternalIdenticalToOperator = (() => T extends (L & T) | T ? true : false) extends < T, >() => T extends (R & T) | T ? true : false @@ -153,10 +223,21 @@ type StrictEqualUsingTSInternalIdenticalToOperator = (() => T extends ( : false : false +/** + * Checks if two types are strictly equal using branding. + */ export type StrictEqualUsingBranding = And< [ExtendsUsingBranding, ExtendsUsingBranding] > +/** + * Represents a type that checks if two types are equal, using + * a hopefully performant approach. + * It first checks if the types are strictly equal using + * {@linkcode StrictEqualUsingTSInternalIdenticalToOperator}. + * If they are not strictly equal, it falls back to using the + * {@linkcode StrictEqualUsingBranding} type. + */ export type HopefullyPerformantEqual = StrictEqualUsingTSInternalIdenticalToOperator< Left, Right @@ -164,7 +245,14 @@ export type HopefullyPerformantEqual = StrictEqualUsingTSInternalId ? true : StrictEqualUsingBranding -export type Params = Actual extends (...args: infer P) => any ? P : never +/** + * Extracts the parameter types from a function type. + */ +export type Params = Actual extends (...args: infer ParameterTypes) => any ? ParameterTypes : never +/** + * Represents the constructor parameters of a class or constructor function. + * If the constructor takes no arguments, an empty array is returned. + */ export type ConstructorParams = Actual extends new (...args: infer P) => any ? Actual extends new () => any ? P | [] @@ -173,9 +261,29 @@ export type ConstructorParams = Actual extends new (...args: infer P) => const mismatch = Symbol('mismatch') type Mismatch = {[mismatch]: 'mismatch'} -/** A type which should match anything passed as a value but *doesn't* match `Mismatch` - helps TypeScript select the right overload for `toEqualTypeOf` and `toMatchTypeOf`. */ + +/** + * A type which should match anything passed as a value but *doesn't* + * match {@linkcode Mismatch}. It helps TypeScript select the right overload + * for {@linkcode PositiveExpectTypeOf.toEqualTypeOf `.toEqualTypeOf()`} and + * {@linkcode PositiveExpectTypeOf.toMatchTypeOf `.toMatchTypeOf()`}. + */ const avalue = Symbol('avalue') +/** + * Represents a value that can be of various types. + */ type AValue = {[avalue]?: undefined} | string | number | boolean | symbol | bigint | null | undefined | void + +/** + * Represents the type of mismatched arguments between + * the actual result and the expected result. + * + * If {@linkcode ActualResult} and {@linkcode ExpectedResult} are equivalent, + * the type resolves to an empty tuple `[]`, indicating no mismatch. + * If they are not equivalent, it resolves to a tuple containing the element + * {@linkcode Mismatch}, signifying a discrepancy between + * the expected and actual results. + */ type MismatchArgs = Eq< ActualResult, ExpectedResult @@ -183,6 +291,9 @@ type MismatchArgs ? [] : [Mismatch] +/** + * Represents the options for the {@linkcode ExpectTypeOf} function. + */ export interface ExpectTypeOfOptions { positive: boolean branded: boolean @@ -193,36 +304,50 @@ type Inverted = {[inverted]: T} const expectNull = Symbol('expectNull') type ExpectNull = {[expectNull]: T; result: ExtendsExcludingAnyOrNever} + const expectUndefined = Symbol('expectUndefined') type ExpectUndefined = {[expectUndefined]: T; result: ExtendsExcludingAnyOrNever} + const expectNumber = Symbol('expectNumber') type ExpectNumber = {[expectNumber]: T; result: ExtendsExcludingAnyOrNever} + const expectString = Symbol('expectString') type ExpectString = {[expectString]: T; result: ExtendsExcludingAnyOrNever} + const expectBoolean = Symbol('expectBoolean') type ExpectBoolean = {[expectBoolean]: T; result: ExtendsExcludingAnyOrNever} + const expectVoid = Symbol('expectVoid') type ExpectVoid = {[expectVoid]: T; result: ExtendsExcludingAnyOrNever} const expectFunction = Symbol('expectFunction') type ExpectFunction = {[expectFunction]: T; result: ExtendsExcludingAnyOrNever any>} + const expectObject = Symbol('expectObject') type ExpectObject = {[expectObject]: T; result: ExtendsExcludingAnyOrNever} + const expectArray = Symbol('expectArray') type ExpectArray = {[expectArray]: T; result: ExtendsExcludingAnyOrNever} + const expectSymbol = Symbol('expectSymbol') type ExpectSymbol = {[expectSymbol]: T; result: ExtendsExcludingAnyOrNever} const expectAny = Symbol('expectAny') type ExpectAny = {[expectAny]: T; result: IsAny} + const expectUnknown = Symbol('expectUnknown') type ExpectUnknown = {[expectUnknown]: T; result: IsUnknown} + const expectNever = Symbol('expectNever') type ExpectNever = {[expectNever]: T; result: IsNever} const expectNullable = Symbol('expectNullable') type ExpectNullable = {[expectNullable]: T; result: Not>>} +/** + * Represents a scolder function that checks if the result of an expecter + * matches the specified options. + */ type Scolder< Expecter extends {result: boolean}, Options extends {positive: boolean}, @@ -232,13 +357,43 @@ type Scolder< ? Expecter : Inverted +/** + * Represents the positive assertion methods available for type checking in the + * {@linkcode expectTypeOf()} utility. + */ export interface PositiveExpectTypeOf extends BaseExpectTypeOf { toEqualTypeOf: { /** - * Uses typescript's internal technique to check for type "identicalness". + * Uses TypeScript's internal technique to check for type "identicalness". + * + * It will check if the types are fully equal to each other. + * It will not fail if two objects have different values, but the same type. + * It will fail however if an object is missing a property. + * + * **_Unexpected failure_**? For a more permissive but less performant + * check that accommodates for equivalent intersection types, + * use {@linkcode branded `.branded.toEqualTypeOf()`}. + * @see {@link https://github.com/mmkal/expect-type#why-is-my-assertion-failing The documentation for details}. * - * **_Unexpected failure_**? For a more permissive but less performant check that accomodates - * for equivalent intersection types, use `.branded`. See [the documentation for details](https://github.com/mmkal/expect-type#why-is-my-assertion-failing). + * @example + * Using generic type argument syntax + * ```ts + * expectTypeOf({ a: 1 }).toEqualTypeOf<{ a: number }>() + * + * expectTypeOf({ a: 1, b: 1 }).not.toEqualTypeOf<{ a: number }>() + * ``` + * + * @example + * Using inferred type syntax by passing a value + * ```ts + * expectTypeOf({ a: 1 }).toEqualTypeOf({ a: 1 }) + * + * expectTypeOf({ a: 1 }).toEqualTypeOf({ a: 2 }) + * ``` + * + * @param value - The value to compare against the expected type. + * @param MISMATCH - The mismatch arguments. + * @returns `true`. */ < Expected extends StrictEqualUsingTSInternalIdenticalToOperator extends true @@ -248,11 +403,37 @@ export interface PositiveExpectTypeOf extends BaseExpectTypeOf, true> ): true + /** - * Uses typescript's internal technique to check for type "identicalness". + * Uses TypeScript's internal technique to check for type "identicalness". + * + * It will check if the types are fully equal to each other. + * It will not fail if two objects have different values, but the same type. + * It will fail however if an object is missing a property. + * + * **_Unexpected failure_**? For a more permissive but less performant + * check that accommodates for equivalent intersection types, + * use {@linkcode branded `.branded.toEqualTypeOf()`}. + * @see {@link https://github.com/mmkal/expect-type#why-is-my-assertion-failing The documentation for details}. + * + * @example + * Using generic type argument syntax + * ```ts + * expectTypeOf({ a: 1 }).toEqualTypeOf<{ a: number }>() + * + * expectTypeOf({ a: 1, b: 1 }).not.toEqualTypeOf<{ a: number }>() + * ``` + * + * @example + * Using inferred type syntax by passing a value + * ```ts + * expectTypeOf({ a: 1 }).toEqualTypeOf({ a: 1 }) + * + * expectTypeOf({ a: 1 }).toEqualTypeOf({ a: 2 }) + * ``` * - * **Unexpected failure**? For a more permissive but less performant check that accomodates - * for equivalent intersection types, use `.branded`. See [the documentation for details](https://github.com/mmkal/expect-type#why-is-my-assertion-failing). + * @param MISMATCH - The mismatch arguments. + * @returns `true`. */ < Expected extends StrictEqualUsingTSInternalIdenticalToOperator extends true @@ -264,23 +445,138 @@ export interface PositiveExpectTypeOf extends BaseExpectTypeOfUsing generic type argument syntax + * ```ts + * expectTypeOf({ a: 1, b: 1 }).toMatchTypeOf<{ a: number }>() + * ``` + * + * @example + * Using inferred type syntax by passing a value + * ```ts + * expectTypeOf({ a: 1, b: 1 }).toMatchTypeOf({ a: 2 }) + * ``` + * + * @param value - The value to compare against the expected type. + * @param MISMATCH - The mismatch arguments. + * @returns `true`. + */ extends true ? unknown : MismatchInfo>( value: Expected & AValue, // reason for `& AValue`: make sure this is only the selected overload when the end-user passes a value for an inferred typearg. The `Mismatch` type does match `AValue`. ...MISMATCH: MismatchArgs, true> ): true + + /** + * A less strict version of {@linkcode toEqualTypeOf `.toEqualTypeOf()`} + * that allows for extra properties. + * This is roughly equivalent to an `extends` constraint + * in a function type argument. + * + * @example + * Using generic type argument syntax + * ```ts + * expectTypeOf({ a: 1, b: 1 }).toMatchTypeOf<{ a: number }>() + * ``` + * + * @example + * Using inferred type syntax by passing a value + * ```ts + * expectTypeOf({ a: 1, b: 1 }).toMatchTypeOf({ a: 2 }) + * ``` + * + * @param MISMATCH - The mismatch arguments. + * @returns `true`. + */ extends true ? unknown : MismatchInfo>( ...MISMATCH: MismatchArgs, true> ): true } - toHaveProperty: ( - key: K, - ...MISMATCH: MismatchArgs, true> - ) => K extends keyof Actual ? PositiveExpectTypeOf : true + /** + * Checks whether an object has a given property. + * + * @example + * check that properties exist + * ```ts + * const obj = {a: 1, b: ''} + * + * expectTypeOf(obj).toHaveProperty('a') + * + * expectTypeOf(obj).not.toHaveProperty('c') + * ``` + * + * @param key - The property key to check for. + * @param MISMATCH - The mismatch arguments. + * @returns `true`. + */ + toHaveProperty: ( + key: KeyType, + ...MISMATCH: MismatchArgs, true> + ) => KeyType extends keyof Actual ? PositiveExpectTypeOf : true + /** + * Inverts the result of the following assertions. + * + * @example + * ```ts + * expectTypeOf({ a: 1 }).not.toMatchTypeOf({ b: 1 }) + * ``` + */ not: NegativeExpectTypeOf + /** + * Intersection types can cause issues with + * {@linkcode toEqualTypeOf `.toEqualTypeOf()`}: + * ```ts + * // ❌ The following line doesn't compile, even though the types are arguably the same. + * expectTypeOf<{ a: 1 } & { b: 2 }>().toEqualTypeOf<{ a: 1; b: 2 }>() + * ``` + * This helper works around this problem by using + * a more permissive but less performant check. + * + * __Note__: This comes at a performance cost, and can cause the compiler + * to 'give up' if used with excessively deep types, so use sparingly. + * + * @see {@link https://github.com/mmkal/expect-type/pull/21 Reference} + */ branded: { + /** + * Uses TypeScript's internal technique to check for type "identicalness". + * + * It will check if the types are fully equal to each other. + * It will not fail if two objects have different values, but the same type. + * It will fail however if an object is missing a property. + * + * **_Unexpected failure_**? For a more permissive but less performant + * check that accommodates for equivalent intersection types, + * use {@linkcode PositiveExpectTypeOf.branded `.branded.toEqualTypeOf()`}. + * @see {@link https://github.com/mmkal/expect-type#why-is-my-assertion-failing The documentation for details}. + * + * @example + * Using generic type argument syntax + * ```ts + * expectTypeOf({ a: 1 }).toEqualTypeOf<{ a: number }>() + * + * expectTypeOf({ a: 1, b: 1 }).not.toEqualTypeOf<{ a: number }>() + * ``` + * + * @example + * Using inferred type syntax by passing a value + * ```ts + * expectTypeOf({ a: 1 }).toEqualTypeOf({ a: 1 }) + * + * expectTypeOf({ a: 1 }).toEqualTypeOf({ a: 2 }) + * ``` + * + * @param MISMATCH - The mismatch arguments. + * @returns `true`. + */ toEqualTypeOf: < Expected extends StrictEqualUsingBranding extends true ? unknown @@ -291,64 +587,556 @@ export interface PositiveExpectTypeOf extends BaseExpectTypeOf extends BaseExpectTypeOf { toEqualTypeOf: { + /** + * Uses TypeScript's internal technique to check for type "identicalness". + * + * It will check if the types are fully equal to each other. + * It will not fail if two objects have different values, but the same type. + * It will fail however if an object is missing a property. + * + * **_Unexpected failure_**? For a more permissive but less performant + * check that accommodates for equivalent intersection types, + * use {@linkcode PositiveExpectTypeOf.branded `.branded.toEqualTypeOf()`}. + * @see {@link https://github.com/mmkal/expect-type#why-is-my-assertion-failing The documentation for details}. + * + * @example + * Using generic type argument syntax + * ```ts + * expectTypeOf({ a: 1 }).toEqualTypeOf<{ a: number }>() + * + * expectTypeOf({ a: 1, b: 1 }).not.toEqualTypeOf<{ a: number }>() + * ``` + * + * @example + * Using inferred type syntax by passing a value + * ```ts + * expectTypeOf({ a: 1 }).toEqualTypeOf({ a: 1 }) + * + * expectTypeOf({ a: 1 }).toEqualTypeOf({ a: 2 }) + * ``` + * + * @param value - The value to compare against the expected type. + * @param MISMATCH - The mismatch arguments. + * @returns `true`. + */ ( value: Expected & AValue, ...MISMATCH: MismatchArgs, false> ): true + + /** + * Uses TypeScript's internal technique to check for type "identicalness". + * + * It will check if the types are fully equal to each other. + * It will not fail if two objects have different values, but the same type. + * It will fail however if an object is missing a property. + * + * **_Unexpected failure_**? For a more permissive but less performant + * check that accommodates for equivalent intersection types, + * use {@linkcode PositiveExpectTypeOf.branded `.branded.toEqualTypeOf()`}. + * @see {@link https://github.com/mmkal/expect-type#why-is-my-assertion-failing The documentation for details}. + * + * @example + * Using generic type argument syntax + * ```ts + * expectTypeOf({ a: 1 }).toEqualTypeOf<{ a: number }>() + * + * expectTypeOf({ a: 1, b: 1 }).not.toEqualTypeOf<{ a: number }>() + * ``` + * + * @example + * Using inferred type syntax by passing a value + * ```ts + * expectTypeOf({ a: 1 }).toEqualTypeOf({ a: 1 }) + * + * expectTypeOf({ a: 1 }).toEqualTypeOf({ a: 2 }) + * ``` + * + * @param MISMATCH - The mismatch arguments. + * @returns `true`. + */ (...MISMATCH: MismatchArgs, false>): true } toMatchTypeOf: { + /** + * A less strict version of + * {@linkcode PositiveExpectTypeOf.toEqualTypeOf `.toEqualTypeOf()`} + * that allows for extra properties. + * This is roughly equivalent to an `extends` constraint + * in a function type argument. + * + * @example + * Using generic type argument syntax + * ```ts + * expectTypeOf({ a: 1, b: 1 }).toMatchTypeOf<{ a: number }>() + * ``` + * + * @example + * Using inferred type syntax by passing a value + * ```ts + * expectTypeOf({ a: 1, b: 1 }).toMatchTypeOf({ a: 2 }) + * ``` + * + * @param value - The value to compare against the expected type. + * @param MISMATCH - The mismatch arguments. + * @returns `true`. + */ ( value: Expected & AValue, // reason for `& AValue`: make sure this is only the selected overload when the end-user passes a value for an inferred typearg. The `Mismatch` type does match `AValue`. ...MISMATCH: MismatchArgs, false> ): true + + /** + * A less strict version of + * {@linkcode PositiveExpectTypeOf.toEqualTypeOf `.toEqualTypeOf()`} + * that allows for extra properties. + * This is roughly equivalent to an `extends` constraint + * in a function type argument. + * + * @example + * Using generic type argument syntax + * ```ts + * expectTypeOf({ a: 1, b: 1 }).toMatchTypeOf<{ a: number }>() + * ``` + * + * @example + * Using inferred type syntax by passing a value + * ```ts + * expectTypeOf({ a: 1, b: 1 }).toMatchTypeOf({ a: 2 }) + * ``` + * + * @param MISMATCH - The mismatch arguments. + * @returns `true`. + */ (...MISMATCH: MismatchArgs, false>): true } - toHaveProperty: ( - key: K, - ...MISMATCH: MismatchArgs, false> + /** + * Checks whether an object has a given property. + * + * @example + * check that properties exist + * ```ts + * const obj = {a: 1, b: ''} + * + * expectTypeOf(obj).toHaveProperty('a') + * + * expectTypeOf(obj).not.toHaveProperty('c') + * ``` + * + * @param key - The property key to check for. + * @param MISMATCH - The mismatch arguments. + * @returns `true`. + */ + toHaveProperty: ( + key: KeyType, + ...MISMATCH: MismatchArgs, false> ) => true } +/** + * Represents a conditional type that selects either + * {@linkcode PositiveExpectTypeOf} or {@linkcode NegativeExpectTypeOf} based + * on the value of the `positive` property in the {@linkcode Options} type. + */ export type ExpectTypeOf = Options['positive'] extends true ? PositiveExpectTypeOf : NegativeExpectTypeOf +/** + * Represents the base interface for the + * {@linkcode expectTypeOf()} function. + * Provides a set of assertion methods to perform type checks on a value. + */ export interface BaseExpectTypeOf { + /** + * Checks whether the type of the value is `any`. + */ toBeAny: Scolder, Options> + + /** + * Checks whether the type of the value is `unknown`. + */ toBeUnknown: Scolder, Options> + + /** + * Checks whether the type of the value is `never`. + */ toBeNever: Scolder, Options> + + /** + * Checks whether the type of the value is `function`. + */ toBeFunction: Scolder, Options> + + /** + * Checks whether the type of the value is `object`. + */ toBeObject: Scolder, Options> + + /** + * Checks whether the type of the value is an {@linkcode Array}. + */ toBeArray: Scolder, Options> + + /** + * Checks whether the type of the value is `number`. + */ toBeNumber: Scolder, Options> + + /** + * Checks whether the type of the value is `string`. + */ toBeString: Scolder, Options> + + /** + * Checks whether the type of the value is `boolean`. + */ toBeBoolean: Scolder, Options> + + /** + * Checks whether the type of the value is `void`. + */ toBeVoid: Scolder, Options> + + /** + * Checks whether the type of the value is `symbol`. + */ toBeSymbol: Scolder, Options> + + /** + * Checks whether the type of the value is `null`. + */ toBeNull: Scolder, Options> + + /** + * Checks whether the type of the value is `undefined`. + */ toBeUndefined: Scolder, Options> + + /** + * Checks whether the type of the value is `null` or `undefined`. + */ toBeNullable: Scolder, Options> + /** + * Checks whether a function is callable with the given parameters. + * + * __Note__: You cannot negate this assertion with + * {@linkcode PositiveExpectTypeOf.not `.not`} you need to use + * `ts-expect-error` instead. + * + * @example + * ```ts + * const f = (a: number) => [a, a] + * + * expectTypeOf(f).toBeCallableWith(1) + * ``` + * + * __Known Limitation__: This assertion will likely fail if you try to use it + * with a generic function or an overload. + * @see {@link https://github.com/mmkal/expect-type/issues/50 This issue} for an example and a workaround. + * + * @param args - The arguments to check for callability. + * @returns `true`. + */ toBeCallableWith: Options['positive'] extends true ? (...args: Params) => true : never + + /** + * Checks whether a class is constructible with the given parameters. + * + * @example + * ```ts + * expectTypeOf(Date).toBeConstructibleWith('1970') + * + * expectTypeOf(Date).toBeConstructibleWith(0) + * + * expectTypeOf(Date).toBeConstructibleWith(new Date()) + * + * expectTypeOf(Date).toBeConstructibleWith() + * ``` + * + * @param args - The arguments to check for constructibility. + * @returns `true`. + */ toBeConstructibleWith: Options['positive'] extends true ? (...args: ConstructorParams) => true : never + + /** + * Equivalent to the {@linkcode Extract} utility type. + * Helps narrow down complex union types. + * + * @example + * ```ts + * type ResponsiveProp = T | T[] | { xs?: T; sm?: T; md?: T } + * + * interface CSSProperties { + * margin?: string + * padding?: string + * } + * + * function getResponsiveProp(_props: T): ResponsiveProp { + * return {} + * } + * + * const cssProperties: CSSProperties = { margin: '1px', padding: '2px' } + * + * expectTypeOf(getResponsiveProp(cssProperties)) + * .extract<{ xs?: any }>() // extracts the last type from a union + * .toEqualTypeOf<{ + * xs?: CSSProperties + * sm?: CSSProperties + * md?: CSSProperties + * }>() + * + * expectTypeOf(getResponsiveProp(cssProperties)) + * .extract() // extracts an array from a union + * .toEqualTypeOf() + * ``` + * + * __Note__: If no type is found in the union, it will return `never`. + * + * @param v - The type to extract from the union. + * @returns The type after extracting the type from the union. + */ extract: (v?: V) => ExpectTypeOf, Options> + + /** + * Equivalent to the {@linkcode Exclude} utility type. + * Removes types from a union. + * + * @example + * ```ts + * type ResponsiveProp = T | T[] | { xs?: T; sm?: T; md?: T } + * + * interface CSSProperties { + * margin?: string + * padding?: string + * } + * + * function getResponsiveProp(_props: T): ResponsiveProp { + * return {} + * } + * + * const cssProperties: CSSProperties = { margin: '1px', padding: '2px' } + * + * expectTypeOf(getResponsiveProp(cssProperties)) + * .exclude() + * .exclude<{ xs?: unknown }>() // or just `.exclude()` + * .toEqualTypeOf() + * ``` + */ exclude: (v?: V) => ExpectTypeOf, Options> - pick: (v?: K) => ExpectTypeOf, Options> - omit: )>(v?: K) => ExpectTypeOf, Options> - parameter: >(number: K) => ExpectTypeOf[K], Options> + + /** + * Equivalent to the {@linkcode Pick} utility type. + * Helps select a subset of properties from an object type. + * + * @example + * ```ts + * interface Person { + * name: string + * age: number + * } + * + * expectTypeOf() + * .pick<'name'>() + * .toEqualTypeOf<{ name: string }>() + * ``` + * + * @param keyToPick - The property key to pick. + * @returns The type after picking the property. + */ + pick: (keyToPick?: KeyToPick) => ExpectTypeOf, Options> + + /** + * Equivalent to the {@linkcode Omit} utility type. + * Helps remove a subset of properties from an object type. + * + * @example + * ```ts + * interface Person { + * name: string + * age: number + * } + * + * expectTypeOf().omit<'name'>().toEqualTypeOf<{ age: number }>() + * ``` + * + * @param keyToOmit - The property key to omit. + * @returns The type after omitting the property. + */ + omit: )>( + keyToOmit?: KeyToOmit, + ) => ExpectTypeOf, Options> + + /** + * Extracts a certain function argument with `.parameter(number)` call to + * perform other assertions on it. + * + * @example + * ```ts + * function foo(a: number, b: string) { + * return [a, b] + * } + * + * expectTypeOf(foo).parameter(0).toBeNumber() + * + * expectTypeOf(foo).parameter(1).toBeString() + * ``` + * + * @param index - The index of the parameter to extract. + * @returns The extracted parameter type. + */ + parameter: >(index: Index) => ExpectTypeOf[Index], Options> + + /** + * Equivalent to the {@linkcode Parameters} utility type. + * Extracts function parameters to perform assertions on its value. + * Parameters are returned as an array. + * + * @example + * ```ts + * function noParam() {} + * + * function hasParam(s: string) {} + * + * expectTypeOf(noParam).parameters.toEqualTypeOf<[]>() + * + * expectTypeOf(hasParam).parameters.toEqualTypeOf<[string]>() + * ``` + */ parameters: ExpectTypeOf, Options> + + /** + * Equivalent to the {@linkcode ConstructorParameters} utility type. + * Extracts constructor parameters as an array of values and + * perform assertions on them with this method. + * + * @example + * ```ts + * expectTypeOf(Date).constructorParameters.toEqualTypeOf< + * [] | [string | number | Date] + * >() + * ``` + */ constructorParameters: ExpectTypeOf, Options> + + /** + * Equivalent to the {@linkcode ThisParameterType} utility type. + * Extracts the `this` parameter of a function to + * perform assertions on its value. + * + * @example + * ```ts + * function greet(this: { name: string }, message: string) { + * return `Hello ${this.name}, here's your message: ${message}` + * } + * + * expectTypeOf(greet).thisParameter.toEqualTypeOf<{ name: string }>() + * ``` + */ thisParameter: ExpectTypeOf, Options> + + /** + * Equivalent to the {@linkcode InstanceType} utility type. + * Extracts the instance type of a class to perform assertions on. + * + * @example + * ```ts + * expectTypeOf(Date).instance.toHaveProperty('toISOString') + * ``` + */ instance: Actual extends new (...args: any[]) => infer I ? ExpectTypeOf : never + + /** + * Equivalent to the {@linkcode ReturnType} utility type. + * Extracts the return type of a function. + * + * @example + * ```ts + * expectTypeOf(() => {}).returns.toBeVoid() + * + * expectTypeOf((a: number) => [a, a]).returns.toEqualTypeOf([1, 2]) + * ``` + */ returns: Actual extends (...args: any[]) => infer R ? ExpectTypeOf : never - resolves: Actual extends PromiseLike ? ExpectTypeOf : never - items: Actual extends ArrayLike ? ExpectTypeOf : never + + /** + * Extracts resolved value of a Promise, + * so you can perform other assertions on it. + * + * @example + * ```ts + * async function asyncFunc() { + * return 123 + * } + * + * expectTypeOf(asyncFunc).returns.resolves.toBeNumber() + * + * expectTypeOf(Promise.resolve('string')).resolves.toBeString() + * ``` + * + * Type Equivalent: + * ```ts + * type Resolves = PromiseType extends PromiseLike + * ? ResolvedType + * : never + * ``` + */ + resolves: Actual extends PromiseLike ? ExpectTypeOf : never + + /** + * Extracts array item type to perform assertions on. + * + * @example + * ```ts + * expectTypeOf([1, 2, 3]).items.toEqualTypeOf() + * + * expectTypeOf([1, 2, 3]).items.not.toEqualTypeOf() + * ``` + * + * __Type Equivalent__: + * ```ts + * type Items = ArrayType extends ArrayLike + * ? ItemType + * : never + * ``` + */ + items: Actual extends ArrayLike ? ExpectTypeOf : never + + /** + * Extracts the type guarded by a function to perform assertions on. + * + * @example + * ```ts + * function isString(v: any): v is string { + * return typeof v === 'string' + * } + * + * expectTypeOf(isString).guards.toBeString() + * ``` + */ guards: Actual extends (v: any, ...args: any[]) => v is infer T ? ExpectTypeOf : never + + /** + * Extracts the type asserted by a function to perform assertions on. + * + * @example + * ```ts + * function assertNumber(v: any): asserts v is number { + * if (typeof v !== 'number') + * throw new TypeError('Nope !') + * } + * + * expectTypeOf(assertNumber).asserts.toBeNumber() + * ``` + */ asserts: Actual extends (v: any, ...args: any[]) => asserts v is infer T ? // Guard methods `(v: any) => asserts v is T` does not actually defines a return type. Thus, any function taking 1 argument matches the signature before. // In case the inferred assertion type `R` could not be determined (so, `unknown`), consider the function as a non-guard, and return a `never` type. @@ -360,8 +1148,23 @@ export interface BaseExpectTypeOf { } const fn: any = () => true +/** + * Represents a function that allows asserting the expected type of a value. + */ export type _ExpectTypeOf = { + /** + * Asserts the expected type of a value. + * + * @param actual - The actual value being asserted. + * @returns An object representing the expected type assertion. + */ (actual: Actual): ExpectTypeOf + + /** + * Asserts the expected type of a value without providing an actual value. + * + * @returns An object representing the expected type assertion. + */ (): ExpectTypeOf }