diff --git a/README.md b/README.md index b148ed7..c7125ba 100644 --- a/README.md +++ b/README.md @@ -282,6 +282,22 @@ type Person = {name: string; age: number} expectTypeOf().omit<'name'>().toEqualTypeOf<{age: number}>() ``` +Use `.readonly` to create a `readonly` version of a type: + +```typescript +type Post = {title: string; content: string} + +expectTypeOf().readonly().toEqualTypeOf>() +``` + +`.readonly` can make specific properties `readonly`: + +```typescript +type Post = {title: string; content: string} + +expectTypeOf().readonly('title').toEqualTypeOf<{readonly title: string; content: string}>() +``` + Make assertions about object properties: ```typescript diff --git a/src/index.ts b/src/index.ts index e787c30..20ecbfb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,7 @@ import type { OverloadReturnTypes, OverloadsNarrowedByParameters, } from './overloads' -import type {StrictEqualUsingTSInternalIdenticalToOperator, AValue, MismatchArgs, Extends} from './utils' +import type {StrictEqualUsingTSInternalIdenticalToOperator, AValue, MismatchArgs, Extends, SetReadonly} from './utils' export * from './branding' // backcompat, consider removing in next major version export * from './utils' // backcompat, consider removing in next major version @@ -654,6 +654,95 @@ export interface BaseExpectTypeOf { keyToOmit?: KeyToOmit, ) => ExpectTypeOf, Options> + /** + * Converts specified properties of an object to `readonly`. + * If no properties are specified, it defaults + * to making all properties `readonly`, similar to the native + * TypeScript {@linkcode Readonly} utility type. + * + * @example + * #### Make all properties `readonly` (default behavior) + * + * ```ts + * import { expectTypeOf } from 'expect-type' + * + * type Post = { + * title: string + * content: string + * } + * + * expectTypeOf().readonly().toEqualTypeOf>() + * ``` + * + * @example + * #### Make specific properties `readonly` + * + * ```ts + * import { expectTypeOf } from 'expect-type' + * + * type Post = { + * title: string + * content: string + * } + * + * expectTypeOf() + * .readonly('title') + * .toEqualTypeOf<{ readonly title: string; content: string }>() + * ``` + * + * @param propertiesToMakeReadonly - The specific properties of the {@linkcode Actual} type to be made `readonly`. If omitted, all properties will be made `readonly`, behaving like the TypeScript {@linkcode Readonly} utility type. + * @returns the type with the specified properties made `readonly`. If no properties are specified, all properties will be made `readonly`, behaving like the TypeScript {@linkcode Readonly} utility. + * + * @template PropertiesToMakeReadonly - The keys of the __`Actual`__ type to be made `readonly`. Defaults to `never`, meaning no specific properties are targeted unless explicitly defined. + * + * @since 1.0.0 + */ + readonly( + propertiesToMakeReadonly: PropertiesToMakeReadonly, + ): ExpectTypeOf, Options> + + /** + * Converts specified properties of an object to `readonly`. + * If no properties are specified, it defaults + * to making all properties `readonly`, similar to the native + * TypeScript {@linkcode Readonly} utility type. + * + * @example + * #### Make all properties `readonly` (default behavior) + * + * ```ts + * import { expectTypeOf } from 'expect-type' + * + * type Post = { + * title: string + * content: string + * } + * + * expectTypeOf().readonly().toEqualTypeOf>() + * ``` + * + * @example + * #### Make specific properties `readonly` + * + * ```ts + * import { expectTypeOf } from 'expect-type' + * + * type Post = { + * title: string + * content: string + * } + * + * expectTypeOf() + * .readonly('title') + * .toEqualTypeOf<{ readonly title: string; content: string }>() + * ``` + * + * @returns the type with the specified properties made `readonly`. If no properties are specified, all properties will be made `readonly`, behaving like the TypeScript {@linkcode Readonly} utility. + * + * @since 1.0.0 + */ + readonly(): ExpectTypeOf, Options> + /** * Extracts a certain function argument with `.parameter(number)` call to * perform other assertions on it. @@ -916,6 +1005,7 @@ export const expectTypeOf: _ExpectTypeOf = ( exclude: expectTypeOf, pick: expectTypeOf, omit: expectTypeOf, + readonly: expectTypeOf, toHaveProperty: expectTypeOf, parameter: expectTypeOf, } diff --git a/src/utils.ts b/src/utils.ts index bd90880..1fe66f2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -120,7 +120,7 @@ type ReadonlyEquivalent = Extends< /** * Checks if one type extends another. Note: this is not quite the same as `Left extends Right` because: * 1. If either type is `never`, the result is `true` iff the other type is also `never`. - * 2. Types are wrapped in a 1-tuple so that union types are not distributed - instead we consider `string | number` to _not_ extend `number`. If we used `Left extends Right` directly you would get `Extends` => `false | true` => `boolean`. + * 2. Types are wrapped in a 1-tuple so that union types are not distributed - instead we consider `string | number` to _not_ extend `number`. If we used `Left extends Right` directly you would get `Extends` =\> `false | true` =\> `boolean`. */ export type Extends = IsNever extends true ? IsNever : [Left] extends [Right] ? true : false @@ -227,3 +227,129 @@ export type TuplifyUnion> = * Convert a union like `1 | 2 | 3` to a tuple like `[1, 2, 3]`. */ export type UnionToTuple = TuplifyUnion + +/** + * An alias for type `{}`. Represents any value that is + * not `null` or `undefined`. It is mostly used for semantic purposes + * to help distinguish between an empty object type and `{}` as + * they are not the same. + * + * @example + * + * ```ts + * import { expectTypeOf } from 'expect-type' + * + * expectTypeOf(42).toMatchTypeOf() + * + * expectTypeOf('Hello').toMatchTypeOf() + * + * // Results in [TS2322 Error]: Type 'null' is not assignable to type '{}'. + * expectTypeOf(null).not.toMatchTypeOf() + * + * // Results in [TS2322 Error]: Type 'undefined' is not assignable to type '{}'. + * expectTypeOf(undefined).not.toMatchTypeOf() + * ``` + * + * @since 1.0.0 + * @internal + */ +type AnyNonNullishValue = NonNullable + +/** + * A utility type that represents any function with any number of arguments + * and any return type. + * + * @example + * + * ```ts + * const log: AnyFunction = (...args: any[]) => console.log(...args) + * + * const sum = ((a: number, b: number): number => a + b) satisfies AnyFunction + * ``` + * + * @since 1.0.0 + * @internal + */ +type AnyFunction = (...args: any[]) => any + +/** + * Useful to flatten the type output to improve type hints shown in editors. + * And also to transform an `interface` into a `type` alias to aide + * with assignability and portability. + * + * @example + * #### Flattening intersected types + * + * ```ts + * import { expectTypeOf } from 'expect-type' + * + * type PositionProps = { + * top: number + * left: number + * } + * + * type SizeProps = { + * width: number + * height: number + * } + * + * expectTypeOf().not.toEqualTypeOf<{ + * top: number + * left: number + * width: number + * height: number + * }>() + * + * expectTypeOf>().toEqualTypeOf<{ + * top: number + * left: number + * width: number + * height: number + * }>() + * ``` + * + * @since 1.0.0 + * @internal + * @see {@link https://github.com/sindresorhus/type-fest/blob/main/source/simplify.d.ts | Source} + */ +type Simplify = TypeToFlatten extends AnyFunction + ? TypeToFlatten + : { + [KeyType in keyof TypeToFlatten]: TypeToFlatten[KeyType] + } & AnyNonNullishValue + +/** + * A utility type that makes specific properties of a given type `readonly`. + * It takes two type parameters: {@linkcode BaseType | the base type} and + * {@linkcode KeysToBecomeReadonly | the keys of the properties} + * that should be made `readonly`. The properties specified by the + * {@linkcode KeysToBecomeReadonly | keys} parameter will become + * `readonly`, while the rest of the type remains unchanged. + * + * @example + * #### Set specific properties to `readonly` + * + * ```ts + * import { expectTypeOf } from 'expect-type' + * + * type Post = { + * author: string + * content: string + * title: string + * } + * + * expectTypeOf>().toEqualTypeOf<{ + * readonly author: string + * content: string + * readonly title: string + * }>() + * ``` + * + * @template BaseType - The base type whose properties will be transformed to `readonly`. + * @template KeysToBecomeReadonly - The keys of the __`BaseType`__ to be made `readonly`. + * + * @since 1.0.0 + */ +export type SetReadonly = BaseType extends unknown + ? Simplify & Readonly>> + : never diff --git a/test/usage.test.ts b/test/usage.test.ts index 6f08643..77876ce 100644 --- a/test/usage.test.ts +++ b/test/usage.test.ts @@ -180,6 +180,18 @@ test('Use `.omit` to remove a set of properties from an object', () => { expectTypeOf().omit<'name'>().toEqualTypeOf<{age: number}>() }) +test('Use `.readonly` to create a `readonly` version of a type', () => { + type Post = {title: string; content: string} + + expectTypeOf().readonly().toEqualTypeOf>() +}) + +test('`.readonly` can make specific properties `readonly`', () => { + type Post = {title: string; content: string} + + expectTypeOf().readonly('title').toEqualTypeOf<{readonly title: string; content: string}>() +}) + test('Make assertions about object properties', () => { const obj = {a: 1, b: ''}