diff --git a/package.json b/package.json index d7a386f..ac33c30 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sindresorhus/is", - "version": "8.1.0", + "version": "8.0.0", "description": "Type check values", "license": "MIT", "repository": "sindresorhus/is", diff --git a/readme.md b/readme.md index 509f75f..b2fb284 100644 --- a/readme.md +++ b/readme.md @@ -786,44 +786,6 @@ handleMovieRatingApiResponse({rating: 0.87, title: 'The Matrix'}); handleMovieRatingApiResponse({rating: '🦄'}); ``` -### Negative assertion - -Asserts that `value` is not the specified type. Only exact, type-safe negative assertions are exposed. - -Supported assertions: - -- `assert.not.undefined(value)` -- `assert.not.null(value)` -- `assert.not.nullOrUndefined(value)` -- `assert.not.string(value)` -- `assert.not.boolean(value)` -- `assert.not.symbol(value)` -- `assert.not.bigint(value)` -- `assert.not.primitive(value)` - -This intentionally excludes checks that cannot produce a safe TypeScript complement: `number` because `is.number` rejects `NaN`, refinements such as `integer` and `validDate`, and branded structural object checks such as `map` and `date`. Broad object checks such as `object` are also excluded to keep negative assertions limited to primitive and nullish types. - -```ts -import {assert} from '@sindresorhus/is'; - -const value: string | undefined = getValue(); - -assert.not.undefined(value); -// Throws if `value` is `undefined`. Otherwise, `value` is now typed as `string`. -``` - -For `unknown` input, exact negative assertions narrow to the remaining representable type: - -```ts -const value: unknown = getValue(); - -assert.not.nullOrUndefined(value); -// `value` is now typed as non-nullish. - -assert.not.primitive(value); -// `value` is now typed as `object`. -``` - ### Optional assertion Asserts that `value` is `undefined` or satisfies the provided `assertion`. diff --git a/source/index.ts b/source/index.ts index 6c2f428..eee1316 100644 --- a/source/index.ts +++ b/source/index.ts @@ -403,13 +403,9 @@ function validatePredicateArray(predicateArray: readonly Predicate[], allowEmpty } for (const predicate of predicateArray) { - validatePredicate(predicate); - } -} - -function validatePredicate(predicate: Predicate) { - if (!isFunction(predicate)) { - throw new TypeError(`Invalid predicate: ${JSON.stringify(predicate)}`); + if (!isFunction(predicate)) { + throw new TypeError(`Invalid predicate: ${JSON.stringify(predicate)}`); + } } } @@ -825,7 +821,7 @@ export function isOddInteger(value: unknown): boolean { } export function isOneOf(values: T): (value: unknown) => value is T[number] { - return (value: unknown): value is T[number] => values.includes(value); + return (value: unknown): value is T[number] => values.includes(value as T[number]); } export function isPlainObject(value: unknown): value is Record { @@ -987,7 +983,9 @@ export function isWhitespaceString(value: unknown): value is Whitespace { type ArrayMethod = (function_: (value: unknown, index: number, array: unknown[]) => boolean, thisArgument?: unknown) => boolean; function predicateOnArray(method: ArrayMethod, predicate: Predicate, values: unknown[]) { - validatePredicate(predicate); + if (!isFunction(predicate)) { + throw new TypeError(`Invalid predicate: ${JSON.stringify(predicate)}`); + } if (values.length === 0) { throw new TypeError('Invalid number of values'); @@ -1000,17 +998,6 @@ function typeErrorMessage(description: AssertionTypeDescription, value: unknown) return `Expected value which is \`${description}\`, received value of type \`${is(value)}\`.`; } -function typeErrorMessageNot(description: AssertionTypeDescription, value: unknown): string { - return `Expected value which is not \`${description}\`, received value of type \`${is(value)}\`.`; -} - -type NotAssertionResult = Exclude & ([unknown] extends [Value] ? UnknownResult : unknown); - -type NotAssertion = (value: Value, message?: string) => asserts value is NotAssertionResult; - -// eslint-disable-next-line @typescript-eslint/no-restricted-types -type UnknownNotPrimitive = Exclude | object; - function unique(values: T[]): T[] { // eslint-disable-next-line unicorn/prefer-spread return Array.from(new Set(values)); @@ -1136,8 +1123,6 @@ type Assert = { directInstanceOf: (instance: unknown, class_: Class, message?: string) => asserts instance is T; inRange: (value: number, range: number | [number, number], message?: string) => asserts value is number; - not: NotAssert; - // Variadic functions. any: (predicate: Predicate | readonly Predicate[], ...values: unknown[]) => void | never; all: (predicate: Predicate | readonly Predicate[], ...values: unknown[]) => void | never; @@ -1150,58 +1135,9 @@ type Assert = { optional: (value: unknown, assertion: (value: unknown, message?: string) => asserts value is T, message?: string) => asserts value is T | undefined; }; -type NotAssert = { - undefined: NotAssertion>; - // eslint-disable-next-line @typescript-eslint/no-restricted-types - null: NotAssertion>; - // eslint-disable-next-line @typescript-eslint/no-restricted-types - nullOrUndefined: NotAssertion>; - string: NotAssertion>; - boolean: NotAssertion>; - symbol: NotAssertion>; - bigint: NotAssertion>; - // eslint-disable-next-line @typescript-eslint/no-restricted-types - primitive: NotAssertion; -}; - -// Negative assertions are limited to types where the assertion rejects every TypeScript value assignable to the forbidden type. Structural object types such as `Map`, `Set`, `Date`, and `Array` are excluded because TypeScript accepts shape-compatible mocks while the runtime checks use object brands, so `Exclude` would narrow values that can pass the negative assertion. -function createAssertNot(predicate: Predicate, description: AssertionTypeDescription): NotAssertion { - return (value: Value, message?: string): asserts value is NotAssertionResult => { - if (predicate(value)) { - throw new TypeError(message ?? typeErrorMessageNot(description, value)); - } - }; -} - -export const assertNotUndefined: NotAssertion> = createAssertNot>(isUndefined, 'undefined'); -// eslint-disable-next-line @typescript-eslint/no-restricted-types -export const assertNotNull: NotAssertion> = createAssertNot>(isNull, 'null'); -// eslint-disable-next-line @typescript-eslint/no-restricted-types -export const assertNotNullOrUndefined: NotAssertion> - // eslint-disable-next-line @typescript-eslint/no-restricted-types - = createAssertNot>(isNullOrUndefined, 'null or undefined'); -export const assertNotString: NotAssertion> = createAssertNot>(isString, 'string'); -export const assertNotBoolean: NotAssertion> = createAssertNot>(isBoolean, 'boolean'); -export const assertNotSymbol: NotAssertion> = createAssertNot>(isSymbol, 'symbol'); -export const assertNotBigint: NotAssertion> = createAssertNot>(isBigint, 'bigint'); -export const assertNotPrimitive: NotAssertion = createAssertNot(isPrimitive, 'primitive'); // eslint-disable-line @typescript-eslint/no-restricted-types - -// We intentionally do not support `assert.not(is.undefined, value)`. TypeScript cannot derive safe complement types from arbitrary predicates, and many predicates here are refinements (for example, `is.number` rejects `NaN`). Explicit methods keep runtime checks and type narrowing aligned. -const notAssertions: NotAssert = { - bigint: assertNotBigint, - boolean: assertNotBoolean, - null: assertNotNull, - nullOrUndefined: assertNotNullOrUndefined, - primitive: assertNotPrimitive, - string: assertNotString, - symbol: assertNotSymbol, - undefined: assertNotUndefined, -}; - export const assert: Assert = { all: assertAll, any: assertAny, - not: notAssertions, optional: assertOptional, array: assertArray, arrayBuffer: assertArrayBuffer, diff --git a/source/utilities.ts b/source/utilities.ts index 686edb1..102b6db 100644 --- a/source/utilities.ts +++ b/source/utilities.ts @@ -1,3 +1,3 @@ export function keysOf>(value: T): Array { - return Object.keys(value) as Array; // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion + return Object.keys(value) as Array; } diff --git a/test/test.ts b/test/test.ts index 2f98bfb..8e64d69 100644 --- a/test/test.ts +++ b/test/test.ts @@ -466,17 +466,6 @@ const subClasses = new Map([ ['object', keysOf(objectTypes)], ]); -const notAssertionFixtures = { - bigint: {fixture: 1n, nonFixture: '🦄', typeDescription: 'bigint'}, - boolean: {fixture: false, nonFixture: '🦄', typeDescription: 'boolean'}, - null: {fixture: null, nonFixture: '🦄', typeDescription: 'null'}, - nullOrUndefined: {fixtures: [null, undefined], nonFixture: '🦄', typeDescription: 'null or undefined'}, - primitive: {fixtures: [false, null, undefined], nonFixture: [], typeDescription: 'primitive'}, - string: {fixture: '🦄', nonFixture: 1, typeDescription: 'string'}, - symbol: {fixture: Symbol('🦄'), nonFixture: '🦄', typeDescription: 'symbol'}, - undefined: {fixture: undefined, nonFixture: null, typeDescription: 'undefined'}, -} as const satisfies Record; - // This ensures a certain method matches only the types it's supposed to and none of the other methods' types for (const type of keysOf(types)) { test(`is.${type}`, () => { @@ -2545,14 +2534,6 @@ test('custom assertion message', () => { isAssert.nativePromise(undefined, message); }); - assertThrowsTypeErrorWithMessage(() => { - isAssert.not.undefined(undefined, message); - }); - - assertThrowsTypeErrorWithMessage(() => { - isAssert.not.string('hello', message); - }); - assertThrowsTypeErrorWithMessage(() => { isAssert.negativeNumber(undefined, message); }); @@ -2734,68 +2715,6 @@ test('custom assertion message', () => { }); }); -test('isAssert.not.undefined', () => { - assert.throws(() => { - isAssert.not.undefined(undefined); - }, { - message: 'Expected value which is not `undefined`, received value of type `undefined`.', - }); - - assert.doesNotThrow(() => { - isAssert.not.undefined(null); - }); - - assert.doesNotThrow(() => { - isAssert.not.undefined(false); - }); - - assert.doesNotThrow(() => { - isAssert.not.undefined(0); - }); - - assert.doesNotThrow(() => { - isAssert.not.undefined(''); - }); -}); - -test('isAssert.not', () => { - assert.deepStrictEqual(new Set(keysOf(isAssert.not)), new Set(keysOf(notAssertionFixtures))); - - for (const type of keysOf(notAssertionFixtures)) { - const {nonFixture, typeDescription} = notAssertionFixtures[type]; - const testAssert = isAssert.not[type]; - const fixtures = 'fixtures' in notAssertionFixtures[type] ? notAssertionFixtures[type].fixtures : [notAssertionFixtures[type].fixture]; - - for (const fixture of fixtures) { - assert.throws(() => { - testAssert(fixture); - }, { - message: `Expected value which is not \`${typeDescription}\`, received value of type \`${is(fixture)}\`.`, - }); - } - - assert.doesNotThrow(() => { - testAssert(nonFixture); - }); - } - - assert.strictEqual('number' in isAssert.not, false); - assert.strictEqual('integer' in isAssert.not, false); - assert.strictEqual('object' in isAssert.not, false); - assert.strictEqual('blob' in isAssert.not, false); - assert.strictEqual('array' in isAssert.not, false); - assert.strictEqual('date' in isAssert.not, false); - assert.strictEqual('function' in isAssert.not, false); - assert.strictEqual('map' in isAssert.not, false); - assert.strictEqual('set' in isAssert.not, false); -}); - -test('isAssert.not edge cases', () => { - assert.doesNotThrow(() => { - isAssert.not.null(undefined); - }); -}); - test('is.optional', () => { assert.ok(is.optional(undefined, is.string)); assert.ok(is.optional('🦄', is.string)); diff --git a/test/type-tests.ts b/test/type-tests.ts index f08fa25..bb50e68 100644 --- a/test/type-tests.ts +++ b/test/type-tests.ts @@ -1,10 +1,4 @@ -import {expectTypeOf} from 'expect-type'; import is, { - assert as isAssert, - assertNotNullOrUndefined, - assertNotPrimitive, - assertNotString, - assertNotUndefined, type EvenInteger, type FiniteNumber, type Integer, @@ -18,14 +12,10 @@ import is, { type PositiveInfinity, type PositiveInteger, type PositiveNumber, - type Primitive, type SafeInteger, type ValidLength, } from '../source/index.ts'; -// eslint-disable-next-line @typescript-eslint/no-restricted-types -type UnknownNotPrimitive = Exclude | object; - // For each predicate, verify two things: // 1. True branch narrows to the branded type. // 2. False branch on a `number` input stays `number` (not `never`). @@ -207,154 +197,6 @@ const distinctNumericBrandsStayDistinct = ( return negativeInteger; }; -const assertNotUndefinedCheck = (value: string | undefined) => { - isAssert.not.undefined(value); - expectTypeOf(value).toEqualTypeOf(); -}; - -const assertNotUndefinedUnknownCheck = (value: unknown) => { - isAssert.not.undefined(value); - expectTypeOf(value).toEqualTypeOf>(); -}; - -const assertNotUndefinedGenericCheck = (value: T) => { - isAssert.not.undefined(value); - const _: Exclude = value; -}; - -const nullValue = null; -type Null = typeof nullValue; - -const assertNotNullUnknownCheck = (value: unknown) => { - isAssert.not.null(value); - expectTypeOf(value).toEqualTypeOf>(); -}; - -const assertNotNullOrUndefinedCheck = (value: string | Null | undefined) => { - isAssert.not.nullOrUndefined(value); - expectTypeOf(value).toEqualTypeOf(); -}; - -const assertNotNullOrUndefinedUnknownCheck = (value: unknown) => { - isAssert.not.nullOrUndefined(value); - expectTypeOf(value).toEqualTypeOf>(); -}; - -const assertNotStringCheck = (value: string | number) => { - isAssert.not.string(value); - expectTypeOf(value).toEqualTypeOf(); -}; - -const assertNotStringUnknownCheck = (value: unknown) => { - isAssert.not.string(value); - expectTypeOf(value).toEqualTypeOf>(); -}; - -const assertNotStringGenericCheck = (value: T) => { - isAssert.not.string(value); - const _: Exclude = value; -}; - -const assertNotBooleanUnknownCheck = (value: unknown) => { - isAssert.not.boolean(value); - expectTypeOf(value).toEqualTypeOf>(); -}; - -const assertNotSymbolUnknownCheck = (value: unknown) => { - isAssert.not.symbol(value); - expectTypeOf(value).toEqualTypeOf>(); -}; - -const assertNotBigintUnknownCheck = (value: unknown) => { - isAssert.not.bigint(value); - expectTypeOf(value).toEqualTypeOf>(); -}; - -const assertNotPrimitiveUnknownCheck = (value: unknown) => { - isAssert.not.primitive(value); - // eslint-disable-next-line @typescript-eslint/no-restricted-types - expectTypeOf(value).toEqualTypeOf(); -}; - -const assertNotPrimitiveGenericCheck = (value: T) => { - isAssert.not.primitive(value); - const _: Exclude = value; -}; - -const assertNotNamedUndefinedExportCheck = (value: 0 | false | '' | Null | undefined | 'ok') => { - assertNotUndefined(value); - expectTypeOf(value).toEqualTypeOf<0 | false | '' | Null | 'ok'>(); -}; - -const assertNotNamedNullOrUndefinedUnknownExportCheck = (value: unknown) => { - assertNotNullOrUndefined(value); - expectTypeOf(value).toEqualTypeOf>(); -}; - -const assertNotNamedStringExportCheck = (value: string | number) => { - assertNotString(value); - expectTypeOf(value).toEqualTypeOf(); -}; - -const assertNotNamedStringUnknownExportCheck = (value: unknown) => { - assertNotString(value); - expectTypeOf(value).toEqualTypeOf>(); -}; - -const assertNotNamedPrimitiveUnknownExportCheck = (value: unknown) => { - assertNotPrimitive(value); - // eslint-disable-next-line @typescript-eslint/no-restricted-types - expectTypeOf(value).toEqualTypeOf(); -}; - -const assertNotCallableDoesNotExistCheck = (value: string | undefined) => { - // @ts-expect-error -- Generic negative assertions cannot safely infer complement types from arbitrary predicates. - isAssert.not(is.undefined, value); - const _: string | undefined = value; -}; - -const assertNotNumberDoesNotExistCheck = (value: string | number) => { - // @ts-expect-error -- `is.number` rejects `NaN`, so a narrowing negative assertion would be unsound. - isAssert.not.number(value); // eslint-disable-line @typescript-eslint/no-unsafe-call - const _: string | number = value; -}; - -const assertNotIntegerDoesNotExistCheck = (value: string | number) => { - // @ts-expect-error -- Numeric refinements are intentionally excluded from `assert.not`. - isAssert.not.integer(value); // eslint-disable-line @typescript-eslint/no-unsafe-call - const _: string | number = value; -}; - -const assertNotObjectDoesNotExistCheck = (value: Record | string) => { - // @ts-expect-error -- TypeScript's `{}` type includes primitives, so `not.object` cannot safely narrow every object-like input. - isAssert.not.object(value); // eslint-disable-line @typescript-eslint/no-unsafe-call - const _: Record | string = value; -}; - -const assertNotBlobDoesNotExistCheck = (value: Blob | File | string) => { - // @ts-expect-error -- `File` extends `Blob` in TypeScript but does not match the exact runtime `Blob` check. - isAssert.not.blob(value); // eslint-disable-line @typescript-eslint/no-unsafe-call - const _: Blob | File | string = value; -}; - -const assertNotMapDoesNotExistCheck = (value: Map | string) => { - // @ts-expect-error -- Structural object types such as `Map` can be assignable in TypeScript without matching the runtime brand check. - isAssert.not.map(value); // eslint-disable-line @typescript-eslint/no-unsafe-call, unicorn/no-array-callback-reference - const _: Map | string = value; -}; - -const assertNotSetDoesNotExistCheck = (value: Set | string) => { - // @ts-expect-error -- Structural object types such as `Set` can be assignable in TypeScript without matching the runtime brand check. - isAssert.not.set(value); // eslint-disable-line @typescript-eslint/no-unsafe-call - const _: Set | string = value; -}; - -const assertNotDateDoesNotExistCheck = (value: Date | string) => { - // @ts-expect-error -- Structural object types such as `Date` can be assignable in TypeScript without matching the runtime brand check. - isAssert.not.date(value); // eslint-disable-line @typescript-eslint/no-unsafe-call - const _: Date | string = value; -}; - // Suppress unused variable warnings nanCheck(42); finiteNumberCheck(42); @@ -376,30 +218,3 @@ integerMixedUnionCheck(1); positiveNumberMixedUnionCheck(1); chainedNumericGuardCheck(1); distinctNumericBrandsStayDistinct(42 as PositiveInteger, -1 as NegativeInteger, 0 as ValidLength); -assertNotUndefinedCheck('🦄'); -assertNotUndefinedUnknownCheck('🦄'); -assertNotUndefinedGenericCheck('🦄'); -assertNotNullUnknownCheck('🦄'); -assertNotNullOrUndefinedCheck('🦄'); -assertNotNullOrUndefinedUnknownCheck('🦄'); -assertNotStringCheck(1); -assertNotStringUnknownCheck(1); -assertNotStringGenericCheck(1); -assertNotBooleanUnknownCheck(1); -assertNotSymbolUnknownCheck(1); -assertNotBigintUnknownCheck(1); -assertNotPrimitiveUnknownCheck({}); -assertNotPrimitiveGenericCheck({unicorn: true}); -assertNotNamedUndefinedExportCheck(0); -assertNotNamedNullOrUndefinedUnknownExportCheck('🦄'); -assertNotNamedStringExportCheck(1); -assertNotNamedStringUnknownExportCheck(1); -assertNotNamedPrimitiveUnknownExportCheck({}); -assertNotCallableDoesNotExistCheck('🦄'); -assertNotNumberDoesNotExistCheck(Number.NaN); -assertNotIntegerDoesNotExistCheck(1.5); -assertNotObjectDoesNotExistCheck('🦄'); -assertNotBlobDoesNotExistCheck('🦄'); -assertNotMapDoesNotExistCheck('🦄'); -assertNotSetDoesNotExistCheck('🦄'); -assertNotDateDoesNotExistCheck('🦄');