diff --git a/etc/types.api.md b/etc/types.api.md index 3668ac8..1b5e561 100644 --- a/etc/types.api.md +++ b/etc/types.api.md @@ -113,11 +113,7 @@ export function createType>(impl: Impl, over export type CustomMessage = undefined | string | ((got: string, input: T, explanation: E) => string); // @public -export type DeepUnbranded = T extends ReadonlyArray ? { - [P in keyof T & number]: DeepUnbranded; -} : T extends Record ? Omit<{ - [P in keyof T]: DeepUnbranded; -}, typeof brands> : Unbranded; +export type DeepUnbranded = T extends readonly [any, ...any[]] | readonly [] ? UnbrandValues> : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends Record ? UnbrandValues> : Unbranded; // @public export const designType: unique symbol; @@ -460,6 +456,9 @@ export { reportError_2 as reportError } // @public export type Result = Success | Failure; +// @public (undocumented) +export type SimpleAcceptVisitor = (type: SimpleType, visitor: Visitor) => R; + // @public export class SimpleType extends BaseTypeImpl { accept(visitor: Visitor): R; @@ -473,8 +472,6 @@ export class SimpleType extends BaseTypeImpl { - // Warning: (ae-forgotten-export) The symbol "SimpleAcceptVisitor" needs to be exported by the entry point index.d.ts - // // (undocumented) acceptVisitor?: SimpleAcceptVisitor; // (undocumented) @@ -564,6 +561,11 @@ export type TypeOfProperties = { // @public export type Unbranded = T extends WithBrands ? Base : T; +// @public (undocumented) +export type UnbrandValues = { + [P in keyof T]: DeepUnbranded; +}; + // @public (undocumented) export const undefinedType: TypeImpl>; diff --git a/markdown/types.deepunbranded.md b/markdown/types.deepunbranded.md index 99cac74..bd663ad 100644 --- a/markdown/types.deepunbranded.md +++ b/markdown/types.deepunbranded.md @@ -9,18 +9,15 @@ Unbrand a given type (recursive). **Signature:** ```typescript -type DeepUnbranded = T extends ReadonlyArray - ? { - [P in keyof T & number]: DeepUnbranded; - } +type DeepUnbranded = T extends readonly [any, ...any[]] | readonly [] + ? UnbrandValues> + : T extends Array + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> : T extends Record - ? Omit< - { - [P in keyof T]: DeepUnbranded; - }, - typeof brands - > + ? UnbrandValues> : Unbranded; ``` -**References:** [DeepUnbranded](./types.deepunbranded.md), [brands](./types.brands.md), [Unbranded](./types.unbranded.md) +**References:** [UnbrandValues](./types.unbrandvalues.md), [Unbranded](./types.unbranded.md), [DeepUnbranded](./types.deepunbranded.md) diff --git a/markdown/types.md b/markdown/types.md index eea728c..a7185f9 100644 --- a/markdown/types.md +++ b/markdown/types.md @@ -121,6 +121,7 @@ Runtime type-validation with derived TypeScript types. | [PropertiesOfTypeTuple](./types.propertiesoftypetuple.md) | | | [PropertyInfo](./types.propertyinfo.md) | Information about a single property of an object-like type including its optionality. | | [Result](./types.result.md) | The result of a type validation. | +| [SimpleAcceptVisitor](./types.simpleacceptvisitor.md) | | | [Simplify](./types.simplify.md) | Flatten the type output to improve type hints as shown in editors. | | [StringViolation](./types.stringviolation.md) | The supported additional checks on string types. | | [The](./types.the.md) | Obtains the TypeScript type of the given runtime Type-checker. Aka [TypeOf](./types.typeof.md). | @@ -132,6 +133,7 @@ Runtime type-validation with derived TypeScript types. | [TypeOf](./types.typeof.md) | Obtains the TypeScript type of the given runtime Type-checker. Aka [The](./types.the.md). | | [TypeOfProperties](./types.typeofproperties.md) | Translates the type of a Properties-object into the proper TypeScript type to be used in user-code. | | [Unbranded](./types.unbranded.md) | Unbrand a given type (not recursive). | +| [UnbrandValues](./types.unbrandvalues.md) | | | [unknownArray](./types.unknownarray.md) | Built-in validator that accepts all arrays. | | [unknownRecord](./types.unknownrecord.md) | Built-in validator that accepts all objects (null is not accepted). | | [ValidationDetails](./types.validationdetails.md) | Information about the performed validation for error-reporting. | diff --git a/markdown/types.simpleacceptvisitor.md b/markdown/types.simpleacceptvisitor.md new file mode 100644 index 0000000..7fda617 --- /dev/null +++ b/markdown/types.simpleacceptvisitor.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@skunkteam/types](./types.md) > [SimpleAcceptVisitor](./types.simpleacceptvisitor.md) + +## SimpleAcceptVisitor type + +**Signature:** + +```typescript +type SimpleAcceptVisitor = (type: SimpleType, visitor: Visitor) => R; +``` + +**References:** [SimpleType](./types.simpletype.md), [Visitor](./types.visitor.md) diff --git a/markdown/types.simpletypeoptions.md b/markdown/types.simpletypeoptions.md index bdd1fb2..0abf100 100644 --- a/markdown/types.simpletypeoptions.md +++ b/markdown/types.simpletypeoptions.md @@ -14,7 +14,7 @@ interface SimpleTypeOptions | Property | Modifiers | Type | Description | | -------------------------------------------------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------- | ------------ | -| [acceptVisitor?](./types.simpletypeoptions.acceptvisitor.md) | | SimpleAcceptVisitor<ResultType, TypeConfig> | _(Optional)_ | +| [acceptVisitor?](./types.simpletypeoptions.acceptvisitor.md) | | [SimpleAcceptVisitor](./types.simpleacceptvisitor.md)<ResultType, TypeConfig> | _(Optional)_ | | [autoCaster?](./types.simpletypeoptions.autocaster.md) | | [BaseTypeImpl](./types.basetypeimpl.md)<ResultType, TypeConfig>\['autoCaster'\] | _(Optional)_ | | [combineConfig?](./types.simpletypeoptions.combineconfig.md) | | [BaseTypeImpl](./types.basetypeimpl.md)<ResultType, TypeConfig>\['combineConfig'\] | _(Optional)_ | | [enumerableLiteralDomain?](./types.simpletypeoptions.enumerableliteraldomain.md) | | [BaseTypeImpl](./types.basetypeimpl.md)<ResultType, TypeConfig>\['enumerableLiteralDomain'\] | _(Optional)_ | diff --git a/markdown/types.unbrandvalues.md b/markdown/types.unbrandvalues.md new file mode 100644 index 0000000..7a4f7de --- /dev/null +++ b/markdown/types.unbrandvalues.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [@skunkteam/types](./types.md) > [UnbrandValues](./types.unbrandvalues.md) + +## UnbrandValues type + +**Signature:** + +```typescript +type UnbrandValues = { + [P in keyof T]: DeepUnbranded; +}; +``` + +**References:** [DeepUnbranded](./types.deepunbranded.md) diff --git a/package-lock.json b/package-lock.json index 476ae14..e60fb37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "eslint": "^8.50.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-import": "^2.28.1", + "expect-type": "^1.1.0", "jest": "^29.7.0", "jest-extended": "^4.0.1", "npm-run-all": "^4.1.5", @@ -4683,6 +4684,15 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/expect-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", + "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/package.json b/package.json index 4e89872..69ca47a 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "eslint": "^8.50.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-import": "^2.28.1", + "expect-type": "^1.1.0", "jest": "^29.7.0", "jest-extended": "^4.0.1", "npm-run-all": "^4.1.5", diff --git a/src/base-type.test.ts b/src/base-type.test.ts index cd87c24..c592406 100644 --- a/src/base-type.test.ts +++ b/src/base-type.test.ts @@ -1,145 +1,124 @@ import assert from 'assert'; +import { expectTypeOf } from 'expect-type'; import { BaseTypeImpl } from './base-type'; import type { The } from './interfaces'; -import { assignableTo, testTypes } from './testutils'; import { boolean, int, literal, number, object, pattern, string, undefinedType, unknownRecord } from './types'; describe(BaseTypeImpl, () => { test.each(['a string', 123, false, { key: 'value' }] as const)('guard value: %p', value => { if (string.is(value)) { - assignableTo<'a string'>(value); - assignableTo('a string'); + expectTypeOf(value).toEqualTypeOf<'a string'>(); expect(value).toBe('a string'); } if (number.is(value)) { - assignableTo<123>(value); - assignableTo(123); + expectTypeOf(value).toEqualTypeOf<123>(); expect(value).toBe(123); } if (boolean.is(value)) { - assignableTo(value); - assignableTo(false); + expectTypeOf(value).toEqualTypeOf(); expect(value).toBe(false); } if (unknownRecord.is(value)) { - assignableTo<{ key: 'value' }>(value); - assignableTo({ key: 'value' }); + expectTypeOf(value).toEqualTypeOf<{ readonly key: 'value' }>(); expect(value).toEqual({ key: 'value' }); } const array = [value, value]; if (array.every(string.is)) { - assignableTo<'a string'[]>(array); - assignableTo(['a string']); + expectTypeOf(array).toEqualTypeOf<'a string'[]>(); expect(array).toEqual(['a string', 'a string']); } else if (array.every(number.is)) { - assignableTo<123[]>(array); - assignableTo([123]); + expectTypeOf(array).toEqualTypeOf<123[]>(); expect(array).toEqual([123, 123]); } else if (array.every(boolean.is)) { - assignableTo(array); - assignableTo([false]); + expectTypeOf(array).toEqualTypeOf(); expect(array).toEqual([false, false]); } else if (array.every(unknownRecord.is)) { - assignableTo<{ key: 'value' }[]>(array); - assignableTo([{ key: 'value' }]); + expectTypeOf(array).toEqualTypeOf<{ readonly key: 'value' }[]>(); expect(array).toEqual([{ key: 'value' }, { key: 'value' }]); } else { assert.fail('should have matched one of the other predicates'); } - testTypes(() => { + () => { string.assert(value); - assignableTo<'a string'>(value); - assignableTo('a string'); - }); + expectTypeOf(value).toEqualTypeOf<'a string'>(); + }; - testTypes(() => { + () => { number.assert(value); - assignableTo<123>(value); - assignableTo(123); - }); + expectTypeOf(value).toEqualTypeOf<123>(); + }; - testTypes(() => { + () => { boolean.assert(value); - assignableTo(value); - assignableTo(false); - }); + expectTypeOf(value).toEqualTypeOf(); + }; - testTypes(() => { + () => { unknownRecord.assert(value); - assignableTo<{ key: 'value' }>(value); - assignableTo({ key: 'value' }); - }); + expectTypeOf(value).toEqualTypeOf<{ readonly key: 'value' }>(); + }; }); test('guard value: unknown', () => { const value = undefined as unknown; + expectTypeOf(value).toBeUnknown(); if (string.is(value)) { - assignableTo(value); - assignableTo('a string'); + expectTypeOf(value).toEqualTypeOf(); } if (number.is(value)) { - assignableTo(value); - assignableTo(123); + expectTypeOf(value).toEqualTypeOf(); } if (boolean.is(value)) { - assignableTo(value); - assignableTo(false); + expectTypeOf(value).toEqualTypeOf(); } if (unknownRecord.is(value)) { - assignableTo(value); - assignableTo({ key: 'value' }); + expectTypeOf(value).toEqualTypeOf(); } expect(undefinedType.is(value)).toBeTrue(); if (undefinedType.is(value)) { - assignableTo(value); - assignableTo(undefined); + expectTypeOf(value).toEqualTypeOf(); } const array = [value, value]; if (array.every(string.is)) { - assignableTo(array); - assignableTo(['a string']); + expectTypeOf(array).toEqualTypeOf(); } else if (array.every(number.is)) { - assignableTo(array); - assignableTo([123]); + expectTypeOf(array).toEqualTypeOf(); } else if (array.every(boolean.is)) { - assignableTo(array); - assignableTo([false]); + expectTypeOf(array).toEqualTypeOf(); } else if (array.every(unknownRecord.is)) { - assignableTo(array); - assignableTo([{ key: 'value' }]); + expectTypeOf(array).toEqualTypeOf(); } else if (array.every(undefinedType.is)) { - assignableTo(array); - assignableTo([undefined]); + expectTypeOf(array).toEqualTypeOf(); } else { assert.fail('should have matched the last predicate'); } - testTypes(() => { + () => { + const value = undefined as unknown; string.assert(value); - assignableTo(value); - assignableTo('a string'); - }); + expectTypeOf(value).toEqualTypeOf(); + }; - testTypes(() => { + () => { + const value = undefined as unknown; number.assert(value); - assignableTo(value); - assignableTo(123); - }); + expectTypeOf(value).toEqualTypeOf(); + }; - testTypes(() => { + () => { + const value = undefined as unknown; boolean.assert(value); - assignableTo(value); - assignableTo(false); - }); + expectTypeOf(value).toEqualTypeOf(); + }; - testTypes(() => { + () => { + const value = undefined as unknown; unknownRecord.assert(value); - assignableTo(value); - assignableTo({ key: 'value' }); - }); + expectTypeOf(value).toEqualTypeOf(); + }; }); test('guard objects', () => { @@ -151,16 +130,16 @@ describe(BaseTypeImpl, () => { | { narrow: string; wide: 'sneaky narrow' } // compatible if narrow happens to be `'value'` | { something: 'else' }; // compatible if narrow and wide are present, we don't know. const obj = { narrow: 'value', wide: 'sneaky narrow' } as UnionOfObjects; + expectTypeOf(obj).toEqualTypeOf(); if (TheType.is(obj)) { - assignableTo<{ narrow: 'value'; wide: 'sneaky narrow' } | { something: 'else'; narrow: 'value'; wide: string }>(obj); - assignableTo({ narrow: 'value', wide: 'sneaky narrow' }); - assignableTo({ something: 'else', narrow: 'value', wide: 'something else' }); + expectTypeOf(obj).branded.toEqualTypeOf< + { narrow: 'value'; wide: 'sneaky narrow' } | { something: 'else'; narrow: 'value'; wide: string } + >(); } const value = { narrow: 'value', wide: 'a literal' as const }; if (TheType.is(value)) { - assignableTo<{ narrow: 'value'; wide: 'a literal' }>(value); - assignableTo({ narrow: 'value', wide: 'a literal' }); + expectTypeOf(value).branded.toEqualTypeOf<{ narrow: 'value'; wide: 'a literal' }>(); } }); @@ -179,16 +158,15 @@ describe(BaseTypeImpl, () => { }), ); - assignableTo({ a: NumericString('123'), b: int(123) }); - // @ts-expect-error because values are not checked - assignableTo({ a: '123', b: 123 }); + expectTypeOf({ a: NumericString('123'), b: int(123) }).toMatchTypeOf(); + expectTypeOf({ a: '123', b: 123 }).not.toMatchTypeOf(); // because values are not checked expect(Obj.literal({ a: '123', b: 123 })).toEqual({ a: '123', b: 123 }); // Parsers are still run expect(Obj.literal({ a: ' 123 ', b: 123 })).toEqual({ a: '123', b: 123 }); - assignableTo(Obj.literal({ a: '123', b: 123 })); + expectTypeOf(Obj.literal({ a: '123', b: 123 })).toEqualTypeOf(); expect(() => Obj.literal({ a: 'abc', b: 1.2 })).toThrowErrorMatchingInlineSnapshot(` "errors in [Obj]: diff --git a/src/interfaces.test.ts b/src/interfaces.test.ts new file mode 100644 index 0000000..635b579 --- /dev/null +++ b/src/interfaces.test.ts @@ -0,0 +1,33 @@ +import { expectTypeOf } from 'expect-type'; +import { DeepUnbranded, Unbranded, WithBrands } from './interfaces'; + +test('DeepUnbranded', () => { + type BrandedString = WithBrands; + expectTypeOf>().toEqualTypeOf(); + + type BrandedObject = WithBrands<{ string: BrandedString }, 'BrandedObject'>; + expectTypeOf>().toEqualTypeOf<{ string: BrandedString }>(); + expectTypeOf>().not.toEqualTypeOf<{ string: string }>(); + expectTypeOf>().toEqualTypeOf<{ string: string }>(); + + type BrandedArray = WithBrands; + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf>(); + + type BrandedReadonlyArray = WithBrands; + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf>(); + + type BrandedEmptyTuple = WithBrands<[], 'BrandedEmptyTuple'>; + expectTypeOf>().toEqualTypeOf<[]>(); + expectTypeOf>().toEqualTypeOf<[]>(); + + type BrandedTuple = WithBrands; + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toHaveProperty('length').toEqualTypeOf<3>(); + // This is the best we can do for now: (note the `toMatchTypeOf` instead of the `toEqualTypeOf`) + expectTypeOf>().toMatchTypeOf(); + + // It should be enough for most cases: + [{ string: 'abc' }, { string: 'abc' }, { string: 'abc' }] satisfies DeepUnbranded; +}); diff --git a/src/interfaces.ts b/src/interfaces.ts index a91d375..4ffda4c 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -214,12 +214,18 @@ export type WithBrands = T & { readonly [brands]: export type Unbranded = T extends WithBrands ? Base : T; /** Unbrand a given type (recursive). */ -export type DeepUnbranded = T extends ReadonlyArray - ? { [P in keyof T & number]: DeepUnbranded } +export type DeepUnbranded = T extends readonly [any, ...any[]] | readonly [] + ? UnbrandValues> + : T extends Array + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> : T extends Record - ? Omit<{ [P in keyof T]: DeepUnbranded }, typeof brands> + ? UnbrandValues> : Unbranded; +export type UnbrandValues = { [P in keyof T]: DeepUnbranded }; + /** * The properties of an object type. * diff --git a/src/simple-type.ts b/src/simple-type.ts index 6fcf350..228a63b 100644 --- a/src/simple-type.ts +++ b/src/simple-type.ts @@ -1,7 +1,7 @@ import { BaseTypeImpl, createType } from './base-type'; import type { BasicType, Result, Type, ValidationOptions, ValidationResult, Visitor } from './interfaces'; -type SimpleAcceptVisitor = (type: SimpleType, visitor: Visitor) => R; +export type SimpleAcceptVisitor = (type: SimpleType, visitor: Visitor) => R; export interface SimpleTypeOptions { enumerableLiteralDomain?: BaseTypeImpl['enumerableLiteralDomain']; diff --git a/src/testutils.ts b/src/testutils.ts index 1286ceb..9bf00c3 100644 --- a/src/testutils.ts +++ b/src/testutils.ts @@ -6,14 +6,6 @@ import type { ArrayType, KeyofType, LiteralType, RecordType, UnionType } from '. import { an, basicType, printValue } from './utils'; import { ValidationError } from './validation-error'; -export function assignableTo(_value: T): void { - // intentionally left blank -} - -export function testTypes(..._args: [msg: string, fn: () => void] | [fn: () => void]): void { - // intentionally left blank -} - export interface TypeTestCase { name: string; type: Type | Type[]; diff --git a/src/types.test.ts b/src/types.test.ts index c1bec48..5dc57e9 100644 --- a/src/types.test.ts +++ b/src/types.test.ts @@ -1,16 +1,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ +import { expectTypeOf } from 'expect-type'; import { autoCast } from './autocast'; import type { DeepUnbranded, MessageDetails, ObjectType, The, Type, Unbranded, Writable } from './interfaces'; -import { - assignableTo, - basicTypeMessage, - createExample, - defaultMessage, - defaultUsualSuspects, - stripped, - testTypeImpl, - testTypes, -} from './testutils'; +import { basicTypeMessage, createExample, defaultMessage, defaultUsualSuspects, stripped, testTypeImpl } from './testutils'; import { array, boolean, int, number, object, string } from './types'; import { partial } from './types/interface'; import { intersection } from './types/intersection'; @@ -688,46 +680,29 @@ testTypeImpl({ // Static types tests -testTypes('TypeOf', () => { - const aString: The = 'a string'; - const aNumber: The = 123; - - assignableTo>('a string'); - assignableTo(aString); - // @ts-expect-error number not assignable to string - assignableTo>(123); - // @ts-expect-error string not assignable to number - assignableTo(aString); - - assignableTo>(123); - assignableTo(aNumber); - - // @ts-expect-error string not assignable to number - assignableTo>('a string'); - // @ts-expect-error number not assignable to string - assignableTo(aNumber); +test('TypeOf', () => { + expectTypeOf(string('abc')).toEqualTypeOf(); + expectTypeOf(number(123)).toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); // Branded values: Percentage is assignable to number, but number is not assignable to Percentage. - assignableTo(Percentage(50)); - assignableTo(Percentage(50)); - // @ts-expect-error number not assignable to Percentage - assignableTo(123); + expectTypeOf(Percentage(50)).toEqualTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().not.toMatchTypeOf(); type WithUser = The; const WithUser = MyGenericWrapper(User); - assignableTo>(WithUser({})); - assignableTo(WithUser({})); - assignableTo({ ok: true, inner: { name: { first: SmallString('first'), last: 'last' }, shoeSize: ShoeSize(5) } }); - assignableTo>({ + expectTypeOf().toEqualTypeOf>(); + expectTypeOf().toEqualTypeOf<{ ok: boolean; inner: User }>(); + expectTypeOf({ ok: true, inner: { name: { first: SmallString('first'), last: 'last' }, shoeSize: ShoeSize(5) }, - }); - assignableTo<{ ok: boolean; inner: User }>({} as MyGenericWrapper); - assignableTo<{ ok: boolean; inner: User }>({} as WithUser); + }).toEqualTypeOf(); }); -testTypes('unbranding and literals', () => { +test('unbranding and literals', () => { User.literal({ name: { first: 'John', @@ -744,10 +719,11 @@ testTypes('unbranding and literals', () => { shoeSize: 48, }, }); - assignableTo>(42); - assignableTo>(42); - assignableTo>({ name: { first: 'John', last: 'Doe' }, shoeSize: 48 }); - assignableTo>({ ok: true, inner: { name: { first: 'John', last: 'Doe' }, shoeSize: 48 } }); + expectTypeOf().not.toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf<{ name: { first: string; last: string }; shoeSize: number }>(); + expectTypeOf>().toEqualTypeOf<{ ok: boolean; inner: DeepUnbranded }>(); type ComplexBrandedScenario = The; const ComplexBrandedScenario = object({ @@ -761,35 +737,31 @@ testTypes('unbranding and literals', () => { .withOptional({ optional: SmallString }) .withConstraint('SpecialObject', () => true); - assignableTo>({ int: 123, array: [{ name: { first: 'first', last: 'last' }, shoeSize: 12 }] }); + expectTypeOf>().toHaveProperty('int').toEqualTypeOf(); + expectTypeOf>().toHaveProperty('optional').toEqualTypeOf(); + expectTypeOf>() + .toHaveProperty('array') + .toEqualTypeOf>(); }); -testTypes('assignability of sub-brands', () => { - assignableTo(Age(123)); - assignableTo(Age(123)); - assignableTo(Age(123)); - // @ts-expect-error int not assignable to Age - assignableTo(int(123)); - // @ts-expect-error number not assignable to Age - assignableTo(123); - - assignableTo(ConfirmedAge(123)); - assignableTo(ConfirmedAge(123)); - assignableTo(ConfirmedAge(123)); - assignableTo(ConfirmedAge(123)); - - // @ts-expect-error number not assignable to CheckedAge - assignableTo(123); - - // @ts-expect-error int not assignable to CheckedAge - assignableTo(int(123)); - - // @ts-expect-error Age not assignable to CheckedAge - assignableTo(Age(123)); +test('assignability of sub-brands', () => { + expectTypeOf(Age(123)).toEqualTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().not.toMatchTypeOf(); + expectTypeOf().not.toMatchTypeOf(); + + expectTypeOf(ConfirmedAge(123)).toEqualTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toMatchTypeOf(); + expectTypeOf().not.toMatchTypeOf(); + expectTypeOf().not.toMatchTypeOf(); + expectTypeOf().not.toMatchTypeOf(); }); -testTypes('usability of assert', () => { - const value = {}; +test('usability of assert', () => { + const value = { a: 'string' }; const MyImplicitType = object('MyImplicitType', { a: string }); const MyExplicitType: Type<{ a: string }> = object('MyExplicitType', { a: string }); @@ -800,21 +772,22 @@ testTypes('usability of assert', () => { MyExplicitType.assert(value); }); -testTypes('type inference', () => { - function elementOfType(_type: Type): T { +test('type inference', () => { + function elementOfType(_type: Type | ObjectType): T { return 0 as any; } - assignableTo(elementOfType(number)); - assignableTo(elementOfType(ConfirmedAge)); - assignableTo(elementOfType(User)); - assignableTo(elementOfType(RestrictedUser)); - assignableTo(elementOfType(NestedFromString)); - assignableTo(elementOfType(ComplexNesting)); - assignableTo(elementOfType(IntersectionTest)); - assignableTo>(elementOfType(MyGenericWrapper(User))); - assignableTo>(elementOfType(GenericAugmentation(User))); - - // @ts-expect-error I still don't know how to fix this for `extendWith`: - assignableTo(elementOfType(Age)); + expectTypeOf(elementOfType(number)).toEqualTypeOf(); + expectTypeOf(elementOfType(ConfirmedAge)).toEqualTypeOf(); + expectTypeOf(elementOfType(User)).toEqualTypeOf(); + expectTypeOf(elementOfType(RestrictedUser)).toEqualTypeOf(); + expectTypeOf(elementOfType(NestedFromString)).toEqualTypeOf(); + expectTypeOf(elementOfType(ComplexNesting)).toEqualTypeOf(); + expectTypeOf(elementOfType(IntersectionTest)).toEqualTypeOf(); + expectTypeOf(elementOfType(MyGenericWrapper(User))).toEqualTypeOf>(); + expectTypeOf(elementOfType(GenericAugmentation(User))).toEqualTypeOf>(); + + // I still don't know how to fix this for `extendWith`: + expectTypeOf(elementOfType(Age)).toEqualTypeOf(); + expectTypeOf(elementOfType(Age)).not.toEqualTypeOf(); }); diff --git a/src/types/array.test.ts b/src/types/array.test.ts index 17f6a59..be38645 100644 --- a/src/types/array.test.ts +++ b/src/types/array.test.ts @@ -1,6 +1,7 @@ +import { expectTypeOf } from 'expect-type'; import { autoCast, autoCastAll } from '../autocast'; import type { The } from '../interfaces'; -import { assignableTo, createExample, defaultUsualSuspects, testTypeImpl, testTypes } from '../testutils'; +import { createExample, defaultUsualSuspects, testTypeImpl } from '../testutils'; import { array } from './array'; import { object } from './interface'; import { undefinedType } from './literal'; @@ -134,30 +135,21 @@ testTypeImpl({ invalidValues: [[[null], 'error in [undefined[]] at <[0]>: expected an undefined, got a null']], }); -testTypes(() => { +test('types', () => { type MyArray = The; const MyArray = array(object({ a: string, b: number })); - assignableTo([ - { a: 'string', b: 1 }, - { a: 'another string', b: 2 }, - ]); - assignableTo>(MyArray(0)); - - // @ts-expect-error wrong element type - assignableTo([1]); + expectTypeOf(MyArray.literal([{ a: 'string', b: 123 }])).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf>(); }); -testTypes('correct inference of arrays of branded types', () => { +test('correct inference of arrays of branded types', () => { type MyBrandedType = The; const MyBrandedType = string.withConfig('MyBrandedType', { minLength: 2, maxLength: 2 }); type MyBrandedTypeArray = The; const MyBrandedTypeArray = array(MyBrandedType); - const brandedArray = MyBrandedTypeArray.literal(['ab']); - - assignableTo(brandedArray); - assignableTo([MyBrandedType.literal('ab')]); - // @ts-expect-error not a branded string - assignableTo(['ab']); + expectTypeOf(MyBrandedTypeArray.literal(['ab'])).toEqualTypeOf(); + expectTypeOf([MyBrandedType.literal('ab')]).toEqualTypeOf(); + expectTypeOf(['ab']).not.toEqualTypeOf(); }); diff --git a/src/types/interface.test.ts b/src/types/interface.test.ts index ff37876..6b397bb 100644 --- a/src/types/interface.test.ts +++ b/src/types/interface.test.ts @@ -1,6 +1,7 @@ +import { expectTypeOf } from 'expect-type'; import { autoCast, autoCastAll } from '../autocast'; import type { OneOrMore, The } from '../interfaces'; -import { assignableTo, defaultUsualSuspects, stripped, testTypeImpl, testTypes } from '../testutils'; +import { defaultUsualSuspects, stripped, testTypeImpl } from '../testutils'; import { boolean } from './boolean'; import { InterfacePickOptions, InterfaceType, object, partial } from './interface'; import { undefinedType } from './literal'; @@ -142,16 +143,15 @@ describe(object, () => { expect(impl).toHaveProperty('name', name); }); - testTypes('type of keys and props', () => { - const { keys, props, propsInfo } = MyType; - assignableTo<{ s: typeof string; n: typeof number }>(props); - assignableTo<{ s: { optional: boolean; type: typeof string }; n: { optional: boolean; type: typeof number } }>(propsInfo); - assignableTo({ n: number, s: string }); - assignableTo>(keys); - assignableTo(['s', 'n']); + test('type of keys and props', () => { + expectTypeOf(MyType).toHaveProperty('keys').toEqualTypeOf>(); + expectTypeOf(MyType).toHaveProperty('props').toEqualTypeOf<{ s: typeof string; n: typeof number }>(); + expectTypeOf(MyType) + .toHaveProperty('propsInfo') + .toEqualTypeOf<{ s: { optional: boolean; type: typeof string }; n: { optional: boolean; type: typeof number } }>(); }); - testTypes('not readonly by default', () => { + test('not readonly by default', () => { const value: MyType = { n: 1, s: 's' }; value.n = 4; value.s = 'str'; @@ -218,28 +218,15 @@ describe('withOptional', () => { expect(() => ValidationsReused.literal({ prop: 'wrong value', other: 'correct' })).toThrow('additional validation failed'); }); - testTypes('type of props and the resulting type', () => { - const { props, propsInfo } = MyTypeWithOptional; - assignableTo<{ s: typeof string; n: typeof number }>(props); - assignableTo({ n: number, s: string, b: boolean }); - assignableTo<{ + test('type of props and the resulting type', () => { + expectTypeOf(MyTypeWithOptional).toHaveProperty('props').toEqualTypeOf<{ s: typeof string; n: typeof number; b: typeof boolean }>(); + expectTypeOf(MyTypeWithOptional).toHaveProperty('propsInfo').toEqualTypeOf<{ s: { optional: boolean; type: typeof string }; n: { optional: boolean; type: typeof number }; b: { optional: boolean; type: typeof boolean }; - }>(propsInfo); - assignableTo({ - s: { optional: false, type: string }, - n: { optional: false, type: number }, - b: { optional: true, type: boolean }, - }); - assignableTo({ s: 'asdf' }); - assignableTo({ n: 123, s: 'asdf' }); - assignableTo({ n: 123, s: 'asdf', b: true }); - // @ts-expect-error because q is not included in the type - assignableTo({ n: 123, s: 'asdf', q: true }); - assignableTo<{ n?: number; s: string; b?: boolean }>(MyTypeWithOptional(0)); - // @ts-expect-error because n is optional now - assignableTo<{ n: number; s: string; b?: boolean }>(MyTypeWithOptional(0)); + }>(); + + expectTypeOf().toEqualTypeOf<{ s: string; n?: number; b?: boolean }>(); }); }); @@ -355,23 +342,14 @@ describe.each(['pick', 'omit'] as const)('%s', method => { ); }); - testTypes('type of props and the resulting type', () => { - const { props, propsInfo } = PickedType; - assignableTo<{ mandatory: typeof string; optional: typeof number }>(props); - assignableTo({ mandatory: string, optional: number }); - assignableTo<{ + test('type of props and the resulting type', () => { + expectTypeOf(PickedType).toHaveProperty('props').toEqualTypeOf<{ mandatory: typeof string; optional: typeof number }>(); + expectTypeOf(PickedType).toHaveProperty('propsInfo').toEqualTypeOf<{ mandatory: { optional: boolean; type: typeof string }; optional: { optional: boolean; type: typeof number }; - }>(propsInfo); - assignableTo({ - mandatory: { optional: false, type: string }, - optional: { optional: false, type: number }, - }); - assignableTo({ mandatory: 'asdf' }); - assignableTo({ mandatory: 'asdf', optional: 123 }); - // @ts-expect-error because q is not included in the type - assignableTo({ mandatory: 'asdf', optional: 123, q: true }); - assignableTo<{ mandatory: string; optional?: number }>(PickedType(0)); + }>(); + + expectTypeOf().toEqualTypeOf<{ mandatory: string; optional?: number }>(); }); method === 'omit' && diff --git a/src/types/intersection.test.ts b/src/types/intersection.test.ts index ede9faa..6644c4b 100644 --- a/src/types/intersection.test.ts +++ b/src/types/intersection.test.ts @@ -1,7 +1,8 @@ +import { expectTypeOf } from 'expect-type'; import { autoCastAll } from '../autocast'; import { BaseTypeImpl } from '../base-type'; import type { The } from '../interfaces'; -import { assignableTo, testTypeImpl, testTypes } from '../testutils'; +import { testTypeImpl } from '../testutils'; import { boolean } from './boolean'; import { object, partial } from './interface'; import { IntersectionOfTypeTuple, intersection } from './intersection'; @@ -91,17 +92,16 @@ describe(intersection, () => { ); }); - testTypes('correct intersection of types', () => { + test('correct intersection of types', () => { type Union = BaseTypeImpl<{ option: 'a' } | { option: 'b' }>; type Name = BaseTypeImpl<{ first: string; last: string }>; type ManualIntersection = ({ option: 'a' } | { option: 'b' }) & { first: string; last: string }; type CalculatedIntersection = IntersectionOfTypeTuple<[Union, Name]>; - assignableTo({} as ManualIntersection); - assignableTo({} as CalculatedIntersection); + expectTypeOf().branded.toEqualTypeOf(); }); - testTypes('mixed "and" and "or"', () => { + test('mixed "and" and "or"', () => { type InnerType = The; const InnerType = object('InnerType', { innerProp: boolean }); @@ -110,42 +110,22 @@ describe(intersection, () => { .and(InnerType) .or(object({ type: literal('nested'), nested: InnerType })); - assignableTo({ type: 'and', innerProp: true }); - assignableTo({ type: 'nested', nested: { innerProp: true } }); - assignableTo<({ type: 'and' } & InnerType) | { type: 'nested'; nested: InnerType }>(TaggedUnionWithAndAndOr(0)); - - // @ts-expect-error because one of the `or` branches is missing - assignableTo<{ type: 'nested'; nested: InnerType }>(TaggedUnionWithAndAndOr(0)); - // @ts-expect-error because the `and` branch is incorrect - assignableTo<({ type: 'and' } & { otherType: string }) | { type: 'nested'; nested: InnerType }>(TaggedUnionWithAndAndOr(0)); + expectTypeOf().toEqualTypeOf< + { type: 'and'; innerProp: boolean } | { type: 'nested'; nested: { innerProp: boolean } } + >(); }); - testTypes('resulting type and props', () => { + test('resulting type and props', () => { type MyIntersection = The; const MyIntersection = intersection([intersection([partial({ a: number })]), object({ b: number }), object({ c: boolean })]); - assignableTo({ b: 1, c: false }); - assignableTo({ a: 1, b: 2, c: true }); - assignableTo<{ a?: number; b: number; c: boolean }>(MyIntersection({})); - - // @ts-expect-error d is unknown property - assignableTo({ a: 1, b: 2, c: true, d: 0 }); + expectTypeOf().toEqualTypeOf<{ a?: number; b: number; c: boolean }>(); - // @ts-expect-error a is optional in MyIntersection - assignableTo<{ a: number }>(MyIntersection({})); - - const { props, propsInfo } = MyIntersection; - assignableTo<{ a: typeof number; b: typeof number; c: typeof boolean }>(props); - assignableTo({ a: number, b: number, c: boolean }); - assignableTo<{ + expectTypeOf(MyIntersection).toHaveProperty('props').toEqualTypeOf<{ a: typeof number; b: typeof number; c: typeof boolean }>(); + expectTypeOf(MyIntersection).toHaveProperty('propsInfo').toEqualTypeOf<{ a: { optional: boolean; type: typeof number }; b: { optional: boolean; type: typeof number }; c: { optional: boolean; type: typeof boolean }; - }>(propsInfo); - assignableTo({ - a: { optional: false, type: number }, - b: { optional: false, type: number }, - c: { optional: false, type: boolean }, - }); + }>(); }); }); diff --git a/src/types/keyof.test.ts b/src/types/keyof.test.ts index 0b3ca2f..72dae6e 100644 --- a/src/types/keyof.test.ts +++ b/src/types/keyof.test.ts @@ -1,6 +1,7 @@ +import { expectTypeOf } from 'expect-type'; import { autoCast } from '../autocast'; import type { The } from '../interfaces'; -import { ValidationErrorForTest, assignableTo, basicTypeMessage, defaultUsualSuspects, testTypeImpl, testTypes } from '../testutils'; +import { ValidationErrorForTest, basicTypeMessage, defaultUsualSuspects, testTypeImpl } from '../testutils'; import { keyof, valueof } from './keyof'; import { literal } from './literal'; import { union } from './union'; @@ -66,22 +67,16 @@ describe(keyof, () => { } }); - testTypes(() => { - assignableTo('yes'); - assignableTo('no'); - assignableTo('maybe'); - assignableTo<'yes' | 'no' | 'maybe'>(YourAnswer(0)); + test('types', () => { + expectTypeOf().toEqualTypeOf<'yes' | 'no' | 'maybe'>(); type AsUnion = The; const AsUnion = union([literal('maybe'), literal('yes'), literal('no')]); - assignableTo(YourAnswer(0)); - assignableTo(AsUnion(0)); + expectTypeOf(YourAnswer.literal('maybe')).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); - const translated = YourAnswer.translate(0); - assignableTo<'trouble' | boolean>(translated); - assignableTo('trouble'); - assignableTo(false); - assignableTo(true); + const translated = YourAnswer.translate('yes'); + expectTypeOf(translated).toEqualTypeOf<'trouble' | boolean>(); }); }); @@ -93,10 +88,11 @@ describe(valueof, () => { const FromStringEnum = valueof(StringEnum); test('compatibility', () => { - assignableTo<'uno' | 'dos'>(FromStringEnum('uno')); - assignableTo>(StringEnum.One); - // @ts-expect-error because TypeScript does not allow assigning correct literals to enum bindings - assignableTo>('one'); + expectTypeOf(FromStringEnum.literal(StringEnum.One)).toEqualTypeOf(); + expectTypeOf().toEqualTypeOf>(); + expectTypeOf(FromStringEnum.literal(StringEnum.One)).toMatchTypeOf<'uno' | 'dos'>(); + // because TypeScript does not allow assigning correct literals to enum bindings + expectTypeOf<'one'>().not.toMatchTypeOf>(); expect(FromStringEnum('dos')).toBe('dos'); expect(FromStringEnum.translate('dos')).toBe('Two'); }); diff --git a/src/types/literal.test.ts b/src/types/literal.test.ts index b6ecb5a..c2edd08 100644 --- a/src/types/literal.test.ts +++ b/src/types/literal.test.ts @@ -1,6 +1,7 @@ +import { expectTypeOf } from 'expect-type'; import { autoCast } from '../autocast'; import type { The } from '../interfaces'; -import { assignableTo, testTypeImpl, testTypes } from '../testutils'; +import { testTypeImpl } from '../testutils'; import { literal, nullType, undefinedType } from './literal'; testTypeImpl({ @@ -125,10 +126,10 @@ testTypeImpl({ validConversions: [['42', 42]], }); -testTypes('literal', () => { +test('literal', () => { type MyLiteral = The; const MyLiteral = literal('some value'); - assignableTo<'some value'>(MyLiteral('some value')); - assignableTo('some value'); + expectTypeOf(MyLiteral('some value')).toEqualTypeOf; + expectTypeOf().toEqualTypeOf<'some value'>(); }); diff --git a/src/types/string.test.ts b/src/types/string.test.ts index 1fd892d..1ca41c9 100644 --- a/src/types/string.test.ts +++ b/src/types/string.test.ts @@ -1,6 +1,7 @@ +import { expectTypeOf } from 'expect-type'; import { autoCast, autoCastAll } from '../autocast'; import type { The } from '../interfaces'; -import { assignableTo, basicTypeMessage, defaultUsualSuspects, testTypeImpl, testTypes } from '../testutils'; +import { basicTypeMessage, defaultUsualSuspects, testTypeImpl } from '../testutils'; import { plural } from '../utils'; import { pattern, string } from './string'; @@ -81,10 +82,10 @@ testTypeImpl({ ], }); -testTypes(() => { - assignableTo(ISODate(0)); - // @ts-expect-error string is not assignable to ISODate - assignableTo('2000-01-01'); +test('types', () => { + expectTypeOf(ISODate('2000-01-01')).toEqualTypeOf(); + expectTypeOf(ISODate('2000-01-01')).toMatchTypeOf(); + expectTypeOf('2000-01-01').not.toMatchTypeOf(); }); testTypeImpl({ diff --git a/src/types/union.test.ts b/src/types/union.test.ts index 44f8267..c0acbd3 100644 --- a/src/types/union.test.ts +++ b/src/types/union.test.ts @@ -1,6 +1,7 @@ +import { expectTypeOf } from 'expect-type'; import { autoCast, autoCastAll } from '../autocast'; import type { DeepUnbranded, The } from '../interfaces'; -import { assignableTo, createExample, defaultUsualSuspects, testTypeImpl } from '../testutils'; +import { createExample, defaultUsualSuspects, testTypeImpl } from '../testutils'; import { boolean } from './boolean'; import { object, partial } from './interface'; import { keyof } from './keyof'; @@ -504,12 +505,13 @@ const BrandedOr1 = BrandedA.or(BrandedB); type BrandedOr2 = The; const BrandedOr2 = BrandedB.or(BrandedA); + test('equivalence between `union()` and `.or()`', () => { // Validating issue #87 - assignableTo(BrandedOr1.literal('A')); - assignableTo(BrandedOr2.literal('A')); - assignableTo(BrandedUnion.literal('A')); - assignableTo(BrandedUnion.literal('A')); - // @ts-expect-error Unbranded literal not assignable to branded literal union type - assignableTo('B'); + expectTypeOf(BrandedOr1.literal('A')).toEqualTypeOf(); + expectTypeOf(BrandedOr2.literal('A')).toEqualTypeOf(); + expectTypeOf(BrandedUnion.literal('A')).toEqualTypeOf(); + expectTypeOf(BrandedUnion.literal('A')).toEqualTypeOf(); + // Unbranded literal not assignable to branded literal union type + expectTypeOf('B').not.toMatchTypeOf(); });