Fix some type guards

This commit is contained in:
Sindre Sorhus 2026-04-09 00:31:33 +07:00
parent cb4ee0e92c
commit 13febb6b01
8 changed files with 715 additions and 205 deletions

View file

@ -15,6 +15,7 @@ import is, {
assert as isAssert,
assertPropertyKey,
type AssertionTypeDescription,
type NaN as NaNType,
type Predicate,
type Primitive,
type TypedArray,
@ -156,7 +157,7 @@ const primitiveTypes = {
safeInteger: {
fixtures: [...reusableFixtures.integer, ...reusableFixtures.safeInteger],
typename: 'number',
typeDescription: 'integer',
typeDescription: 'safe integer',
},
infinite: {
fixtures: [...reusableFixtures.infinite],
@ -208,6 +209,7 @@ const objectTypes = {
object: {
fixtures: [
Object.create({x: 1}),
{[Symbol.toStringTag]: 'String'},
...reusableFixtures.plainObject,
],
typename: 'Object',
@ -280,6 +282,7 @@ const objectTypes = {
boundFunction: {
fixtures: [...reusableFixtures.boundFunction, ...reusableFixtures.asyncFunction],
typename: 'Function',
typeDescription: 'bound Function',
},
map: {
fixtures: [
@ -526,6 +529,7 @@ test('is.positiveNumber', () => {
assert.strictEqual(is.positiveNumber(-6), false);
assert.strictEqual(is.positiveNumber(-1.4), false);
assert.strictEqual(is.positiveNumber(Number.NEGATIVE_INFINITY), false);
assert.strictEqual(is.positiveNumber(Number.NaN), false);
assert.throws(() => {
isAssert.positiveNumber(0);
@ -544,6 +548,33 @@ test('is.positiveNumber', () => {
});
});
test('is.nan', () => {
assert.ok(is.nan(Number.NaN));
assert.ok(is.nan(NaN)); // eslint-disable-line unicorn/prefer-number-properties
assert.doesNotThrow(() => {
isAssert.nan(Number.NaN);
});
assert.strictEqual(is.nan(0), false);
assert.strictEqual(is.nan(-0), false);
assert.strictEqual(is.nan(1), false);
assert.strictEqual(is.nan(Number.POSITIVE_INFINITY), false);
assert.strictEqual(is.nan(Number.NEGATIVE_INFINITY), false);
assert.strictEqual(is.nan('NaN'), false);
assert.strictEqual(is.nan(undefined), false);
assert.throws(() => {
isAssert.nan(0);
});
assert.throws(() => {
isAssert.nan(1);
});
assert.throws(() => {
isAssert.nan('NaN');
});
});
test('is.finiteNumber', () => {
assert.ok(is.finiteNumber(6));
assert.ok(is.finiteNumber(-6));
@ -592,6 +623,7 @@ test('is.negativeNumber', () => {
assert.strictEqual(is.negativeNumber(6), false);
assert.strictEqual(is.negativeNumber(1.4), false);
assert.strictEqual(is.negativeNumber(Number.POSITIVE_INFINITY), false);
assert.strictEqual(is.negativeNumber(Number.NaN), false);
assert.throws(() => {
isAssert.negativeNumber(0);
@ -729,6 +761,32 @@ test('is.nonNegativeInteger', () => {
});
});
test('is.infinite', () => {
assert.ok(is.infinite(Number.POSITIVE_INFINITY));
assert.ok(is.infinite(Number.NEGATIVE_INFINITY));
assert.doesNotThrow(() => {
isAssert.infinite(Number.POSITIVE_INFINITY);
});
assert.doesNotThrow(() => {
isAssert.infinite(Number.NEGATIVE_INFINITY);
});
assert.strictEqual(is.infinite(0), false);
assert.strictEqual(is.infinite(1), false);
assert.strictEqual(is.infinite(-1), false);
assert.strictEqual(is.infinite(Number.NaN), false);
assert.strictEqual(is.infinite(Number.MAX_VALUE), false);
assert.strictEqual(is.infinite('Infinity'), false);
assert.throws(() => {
isAssert.infinite(0);
});
assert.throws(() => {
isAssert.infinite(Number.NaN);
});
});
test('is.numericString supplemental', () => {
assert.strictEqual(is.numericString(''), false);
assert.strictEqual(is.numericString(' '), false);
@ -998,6 +1056,20 @@ test('is.urlString', () => {
}
})();
// Type test for is.nan branded-type narrowing
(() => {
const value: unknown = Number.NaN;
if (is.nan(value)) {
// ✅ In true branch: value is narrowed to the branded NaN type
expectTypeOf(value).toEqualTypeOf<NaNType>();
expectTypeOf(value).toMatchTypeOf<number>();
} else {
// ✅ In false branch: value remains unknown (not incorrectly narrowed)
expectTypeOf(value).toEqualTypeOf<unknown>();
}
})();
test('is.truthy', () => {
assert.ok(is.truthy('unicorn'));
assert.ok(is.truthy('🦄'));
@ -2268,370 +2340,379 @@ test('assert', () => {
test('custom assertion message', () => {
const message = 'Custom error message';
assert.throws(() => {
const assertThrowsTypeErrorWithMessage = (assertion: () => void) => {
// `node:assert` does not verify the error class when matching only on `{message}`.
assert.throws(assertion, error => {
assert.ok(error instanceof TypeError);
assert.strictEqual(error.message, message);
return true;
});
};
assertThrowsTypeErrorWithMessage(() => {
isAssert.array(undefined, undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.arrayBuffer(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.arrayLike(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.asyncFunction(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.asyncGenerator(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.asyncGeneratorFunction(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.asyncIterable(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.bigInt64Array(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.bigUint64Array(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.bigint(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.blob(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.boolean(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.boundFunction(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.buffer(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.class(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.dataView(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.date(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.directInstanceOf(undefined, Error, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.emptyArray(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.emptyMap(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.emptyObject(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.emptySet(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.emptyString(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.emptyStringOrWhitespace(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
enum Enum {}
isAssert.enumCase('invalid', Enum, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.error(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.evenInteger(33, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.falsy(true, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.finiteNumber(Number.POSITIVE_INFINITY, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.float32Array(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.float64Array(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.formData(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.function(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.generator(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.generatorFunction(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.htmlElement(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.inRange(5, [1, 2], message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.infinite(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.int16Array(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.int32Array(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.int8Array(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.integer(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.iterable(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.map(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.nan(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.nativePromise(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.negativeNumber(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.nodeStream(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.nonEmptyArray(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.nonEmptyMap(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.nonEmptyObject(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.nonEmptySet(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.nonEmptyString(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.nonEmptyStringAndNotWhitespace(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.nonNegativeNumber(-1, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.null(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.nullOrUndefined(false, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.number(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.numericString(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.object(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.observable(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.oddInteger(42, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.plainObject(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.positiveInteger(0, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.positiveNumber(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.primitive([], message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.promise(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.propertyKey(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.regExp(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.safeInteger(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.set(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.sharedArrayBuffer(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.string(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.symbol(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.truthy(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.tupleLike(undefined, [], message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.typedArray(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.uint16Array(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.uint32Array(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.uint8Array(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.uint8ClampedArray(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.undefined(false, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.urlInstance(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.urlSearchParams(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.urlString(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.validDate(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.validLength(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.weakMap(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.weakRef(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.weakSet(undefined, message);
}, {message});
});
assert.throws(() => {
assertThrowsTypeErrorWithMessage(() => {
isAssert.whitespaceString(undefined, message);
}, {message});
});
});
test('is.optional', () => {

12
test/tsconfig.json Normal file
View file

@ -0,0 +1,12 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "..",
"noUnusedLocals": false,
"noUnusedParameters": false
},
"include": [
"../source",
"type-tests.ts"
]
}

220
test/type-tests.ts Normal file
View file

@ -0,0 +1,220 @@
import is, {
type EvenInteger,
type FiniteNumber,
type Integer,
type NaN as NaNType,
type NegativeInfinity,
type NegativeInteger,
type NegativeNumber,
type NonNegativeInteger,
type NonNegativeNumber,
type OddInteger,
type PositiveInfinity,
type PositiveInteger,
type PositiveNumber,
type SafeInteger,
type ValidLength,
} from '../source/index.ts';
// 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`).
// Without the branded types, `Exclude<number, number>` = `never` would break
// the common validation-guard pattern: if (!is.X(n)) throw; use(n).
const nanCheck = (value: number) => {
if (is.nan(value)) {
const _: NaNType = value;
} else {
const _: number = value;
}
};
const finiteNumberCheck = (value: number) => {
if (is.finiteNumber(value)) {
const _: FiniteNumber = value;
} else {
const _: number = value;
}
};
const nonNegativeNumberCheck = (value: number) => {
if (is.nonNegativeNumber(value)) {
const _: NonNegativeNumber = value;
} else {
const _: number = value;
}
};
const positiveIntegerCheck = (value: number) => {
if (is.positiveInteger(value)) {
const _: PositiveInteger = value;
const __: Integer = value;
const ___: NonNegativeInteger = value;
} else {
const _: number = value;
}
};
const negativeIntegerCheck = (value: number) => {
if (is.negativeInteger(value)) {
const _: NegativeInteger = value;
const __: Integer = value;
} else {
const _: number = value;
}
};
const nonNegativeIntegerCheck = (value: number) => {
if (is.nonNegativeInteger(value)) {
const _: NonNegativeInteger = value;
const __: Integer = value;
} else {
const _: number = value;
}
};
const infiniteCheck = (value: number) => {
if (is.infinite(value)) {
const _: PositiveInfinity | NegativeInfinity = value;
const __: PositiveNumber | NegativeNumber = value;
} else {
const _: number = value;
}
};
const integerCheck = (value: number) => {
if (is.integer(value)) {
const _: Integer = value;
const __: FiniteNumber = value;
} else {
const _: number = value;
}
};
const safeIntegerCheck = (value: number) => {
if (is.safeInteger(value)) {
const _: SafeInteger = value;
const __: Integer = value;
} else {
const _: number = value;
}
};
const evenIntegerCheck = (value: number) => {
if (is.evenInteger(value)) {
const _: EvenInteger = value;
const __: Integer = value;
} else {
const _: number = value;
}
};
const oddIntegerCheck = (value: number) => {
if (is.oddInteger(value)) {
const _: OddInteger = value;
const __: Integer = value;
} else {
const _: number = value;
}
};
const positiveNumberCheck = (value: number) => {
if (is.positiveNumber(value)) {
const _: PositiveNumber = value;
const __: NonNegativeNumber = value;
} else {
const _: number = value;
}
};
const negativeNumberCheck = (value: number) => {
if (is.negativeNumber(value)) {
const _: NegativeNumber = value;
} else {
const _: number = value;
}
};
const validLengthCheck = (value: number) => {
if (is.validLength(value)) {
const _: ValidLength = value;
const __: SafeInteger = value;
const ___: NonNegativeInteger = value;
} else {
const _: number = value;
}
};
const integerUnknownCheck = (value: unknown) => {
if (is.integer(value)) {
const _: Integer = value;
const __: FiniteNumber = value;
}
};
const positiveIntegerUnknownCheck = (value: unknown) => {
if (is.positiveInteger(value)) {
const _: PositiveInteger = value;
const __: NonNegativeInteger = value;
}
};
const integerMixedUnionCheck = (value: string | number) => {
if (is.integer(value)) {
const _: number = value;
} else {
const _: string = value;
}
};
const positiveNumberMixedUnionCheck = (value: string | number) => {
if (is.positiveNumber(value)) {
const _: number = value;
} else {
const _: string = value;
}
};
const chainedNumericGuardCheck = (value: number) => {
if (is.positiveNumber(value) && is.integer(value)) {
const _: PositiveNumber = value;
const __: Integer = value;
const ___: FiniteNumber = value;
}
};
const distinctNumericBrandsStayDistinct = (
positiveInteger: PositiveInteger,
negativeInteger: NegativeInteger,
validLength: ValidLength,
) => {
// @ts-expect-error -- Distinct numeric refinements must not collapse into each other.
const _: NegativeInteger = positiveInteger;
// @ts-expect-error -- ValidLength is non-negative and must not become a signed integer refinement.
const __: NegativeInteger = validLength;
return negativeInteger;
};
// Suppress unused variable warnings
nanCheck(42);
finiteNumberCheck(42);
nonNegativeNumberCheck(42);
positiveIntegerCheck(42);
negativeIntegerCheck(-1);
nonNegativeIntegerCheck(0);
infiniteCheck(Number.POSITIVE_INFINITY);
integerCheck(1);
safeIntegerCheck(1);
evenIntegerCheck(2);
oddIntegerCheck(1);
positiveNumberCheck(1);
negativeNumberCheck(-1);
validLengthCheck(0);
integerUnknownCheck(1);
positiveIntegerUnknownCheck(1);
integerMixedUnionCheck(1);
positiveNumberMixedUnionCheck(1);
chainedNumericGuardCheck(1);
distinctNumericBrandsStayDistinct(42 as PositiveInteger, -1 as NegativeInteger, 0 as ValidLength);