Fix some type guards
This commit is contained in:
parent
cb4ee0e92c
commit
13febb6b01
8 changed files with 715 additions and 205 deletions
116
source/index.ts
116
source/index.ts
|
|
@ -1,14 +1,29 @@
|
|||
import type {
|
||||
ArrayLike,
|
||||
Class,
|
||||
EvenInteger,
|
||||
Falsy,
|
||||
FiniteNumber,
|
||||
Integer,
|
||||
NaN as NaNType,
|
||||
NegativeInfinity,
|
||||
NegativeInteger,
|
||||
NegativeNumber,
|
||||
NodeStream,
|
||||
NonEmptyString,
|
||||
NonNegativeInteger,
|
||||
NonNegativeNumber,
|
||||
ObservableLike,
|
||||
OddInteger,
|
||||
Predicate,
|
||||
Primitive,
|
||||
PositiveInfinity,
|
||||
PositiveInteger,
|
||||
PositiveNumber,
|
||||
SafeInteger,
|
||||
TypedArray,
|
||||
UrlString,
|
||||
ValidLength,
|
||||
WeakRef,
|
||||
Whitespace,
|
||||
} from './types.ts';
|
||||
|
|
@ -22,6 +37,15 @@ type ExtractFromGlobalConstructors<Name extends string> =
|
|||
|
||||
type NodeBuffer = ExtractFromGlobalConstructors<'Buffer'>;
|
||||
|
||||
type NumericGuardResult<Input, Branded extends number> =
|
||||
(
|
||||
unknown extends Input
|
||||
? Branded
|
||||
: Input extends number
|
||||
? Branded & Input
|
||||
: number
|
||||
) & Input;
|
||||
|
||||
const typedArrayTypeNames = [
|
||||
'Int8Array',
|
||||
'Uint8Array',
|
||||
|
|
@ -99,6 +123,7 @@ function isPrimitiveTypeName(name: unknown): name is PrimitiveTypeName {
|
|||
export type TypeName = ObjectTypeName | PrimitiveTypeName;
|
||||
|
||||
const assertionTypeDescriptions = [
|
||||
'bound Function',
|
||||
'positive number',
|
||||
'negative number',
|
||||
'Class',
|
||||
|
|
@ -139,6 +164,7 @@ const assertionTypeDescriptions = [
|
|||
'non-negative number',
|
||||
'odd integer',
|
||||
'positive integer',
|
||||
'safe integer',
|
||||
'T',
|
||||
'in range',
|
||||
'predicate returns truthy for any value',
|
||||
|
|
@ -225,8 +251,7 @@ function detect(value: unknown): TypeName {
|
|||
return 'Promise';
|
||||
}
|
||||
|
||||
const objectTag = Object.prototype.toString.call(value).slice(8, -1);
|
||||
if (objectTag === 'String' || objectTag === 'Boolean' || objectTag === 'Number') {
|
||||
if (isBoxedPrimitiveObject(value)) {
|
||||
throw new TypeError('Please don\'t use object wrappers for primitive types');
|
||||
}
|
||||
|
||||
|
|
@ -237,6 +262,23 @@ function hasPromiseApi<T = unknown>(value: unknown): value is Promise<T> {
|
|||
return isFunction((value as Promise<T>)?.then) && isFunction((value as Promise<T>)?.catch);
|
||||
}
|
||||
|
||||
function hasBoxedPrimitiveBrand(value: unknown, valueOf: () => unknown): boolean {
|
||||
try {
|
||||
// `Object.prototype.toString` can be spoofed via `Symbol.toStringTag`, but the
|
||||
// boxed primitive `valueOf` methods still enforce the real internal brand.
|
||||
Reflect.apply(valueOf, value, []);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isBoxedPrimitiveObject(value: unknown): boolean {
|
||||
return hasBoxedPrimitiveBrand(value, String.prototype.valueOf)
|
||||
|| hasBoxedPrimitiveBrand(value, Boolean.prototype.valueOf)
|
||||
|| hasBoxedPrimitiveBrand(value, Number.prototype.valueOf);
|
||||
}
|
||||
|
||||
const is = Object.assign(
|
||||
detect,
|
||||
{
|
||||
|
|
@ -560,11 +602,13 @@ export function isEnumCase<T = unknown>(value: unknown, targetEnum: T): value is
|
|||
}
|
||||
|
||||
export function isError(value: unknown): value is Error {
|
||||
// TODO: Use `Error.isError` when targeting Node.js 24.`
|
||||
// TODO: Use `Error.isError` when targeting Node.js 24.
|
||||
return getObjectType(value) === 'Error';
|
||||
}
|
||||
|
||||
export function isEvenInteger(value: unknown): value is number {
|
||||
// For numeric guards, preserve branded narrowing for `unknown`, keep the false branch usable for plain `number`, and still narrow mixed unions to `number`.
|
||||
export function isEvenInteger<Input>(value: Input): value is NumericGuardResult<Input, EvenInteger>;
|
||||
export function isEvenInteger(value: unknown): boolean {
|
||||
return isAbsoluteModule2(0)(value);
|
||||
}
|
||||
|
||||
|
|
@ -573,7 +617,8 @@ export function isFalsy(value: unknown): value is Falsy {
|
|||
return !value;
|
||||
}
|
||||
|
||||
export function isFiniteNumber(value: unknown): value is number {
|
||||
export function isFiniteNumber<Input>(value: Input): value is NumericGuardResult<Input, FiniteNumber>;
|
||||
export function isFiniteNumber(value: unknown): boolean {
|
||||
return Number.isFinite(value);
|
||||
}
|
||||
|
||||
|
|
@ -622,7 +667,8 @@ export function isHtmlElement(value: unknown): value is HTMLElement {
|
|||
&& DOM_PROPERTIES_TO_CHECK.every(property => property in value);
|
||||
}
|
||||
|
||||
export function isInfinite(value: unknown): value is number {
|
||||
export function isInfinite<Input>(value: Input): value is NumericGuardResult<Input, PositiveInfinity | NegativeInfinity>;
|
||||
export function isInfinite(value: unknown): boolean {
|
||||
return value === Number.POSITIVE_INFINITY || value === Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
|
||||
|
|
@ -654,7 +700,8 @@ export function isInt8Array(value: unknown): value is Int8Array {
|
|||
return getObjectType(value) === 'Int8Array';
|
||||
}
|
||||
|
||||
export function isInteger(value: unknown): value is number {
|
||||
export function isInteger<Input>(value: Input): value is NumericGuardResult<Input, Integer>;
|
||||
export function isInteger(value: unknown): boolean {
|
||||
return Number.isInteger(value);
|
||||
}
|
||||
|
||||
|
|
@ -666,7 +713,8 @@ export function isMap<Key = unknown, Value = unknown>(value: unknown): value is
|
|||
return getObjectType(value) === 'Map';
|
||||
}
|
||||
|
||||
export function isNan(value: unknown) {
|
||||
export function isNan<Input>(value: Input): value is NumericGuardResult<Input, NaNType>;
|
||||
export function isNan(value: unknown): boolean {
|
||||
return Number.isNaN(value);
|
||||
}
|
||||
|
||||
|
|
@ -674,11 +722,13 @@ export function isNativePromise<T = unknown>(value: unknown): value is Promise<T
|
|||
return getObjectType(value) === 'Promise';
|
||||
}
|
||||
|
||||
export function isNegativeInteger(value: unknown): value is number {
|
||||
export function isNegativeInteger<Input>(value: Input): value is NumericGuardResult<Input, NegativeInteger>;
|
||||
export function isNegativeInteger(value: unknown): boolean {
|
||||
return isInteger(value) && value < 0;
|
||||
}
|
||||
|
||||
export function isNegativeNumber(value: unknown): value is number {
|
||||
export function isNegativeNumber<Input>(value: Input): value is NumericGuardResult<Input, NegativeNumber>;
|
||||
export function isNegativeNumber(value: unknown): boolean {
|
||||
return isNumber(value) && value < 0;
|
||||
}
|
||||
|
||||
|
|
@ -714,11 +764,13 @@ export function isNonEmptyStringAndNotWhitespace(value: unknown): value is NonEm
|
|||
return isString(value) && !isEmptyStringOrWhitespace(value);
|
||||
}
|
||||
|
||||
export function isNonNegativeInteger(value: unknown): value is number {
|
||||
export function isNonNegativeInteger<Input>(value: Input): value is NumericGuardResult<Input, NonNegativeInteger>;
|
||||
export function isNonNegativeInteger(value: unknown): boolean {
|
||||
return isInteger(value) && value >= 0;
|
||||
}
|
||||
|
||||
export function isNonNegativeNumber(value: unknown): value is number {
|
||||
export function isNonNegativeNumber<Input>(value: Input): value is NumericGuardResult<Input, NonNegativeNumber>;
|
||||
export function isNonNegativeNumber(value: unknown): boolean {
|
||||
return isNumber(value) && value >= 0;
|
||||
}
|
||||
|
||||
|
|
@ -763,7 +815,8 @@ export function isObservable(value: unknown): value is ObservableLike {
|
|||
return false;
|
||||
}
|
||||
|
||||
export function isOddInteger(value: unknown): value is number {
|
||||
export function isOddInteger<Input>(value: Input): value is NumericGuardResult<Input, OddInteger>;
|
||||
export function isOddInteger(value: unknown): boolean {
|
||||
return isAbsoluteModule2(1)(value);
|
||||
}
|
||||
|
||||
|
|
@ -783,11 +836,13 @@ export function isPlainObject<Value = unknown>(value: unknown): value is Record<
|
|||
return (prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null) && !(Symbol.toStringTag in value) && !(Symbol.iterator in value);
|
||||
}
|
||||
|
||||
export function isPositiveInteger(value: unknown): value is number {
|
||||
export function isPositiveInteger<Input>(value: Input): value is NumericGuardResult<Input, PositiveInteger>;
|
||||
export function isPositiveInteger(value: unknown): boolean {
|
||||
return isInteger(value) && value > 0;
|
||||
}
|
||||
|
||||
export function isPositiveNumber(value: unknown): value is number {
|
||||
export function isPositiveNumber<Input>(value: Input): value is NumericGuardResult<Input, PositiveNumber>;
|
||||
export function isPositiveNumber(value: unknown): boolean {
|
||||
return isNumber(value) && value > 0;
|
||||
}
|
||||
|
||||
|
|
@ -808,7 +863,8 @@ export function isRegExp(value: unknown): value is RegExp {
|
|||
return getObjectType(value) === 'RegExp';
|
||||
}
|
||||
|
||||
export function isSafeInteger(value: unknown): value is number {
|
||||
export function isSafeInteger<Input>(value: Input): value is NumericGuardResult<Input, SafeInteger>;
|
||||
export function isSafeInteger(value: unknown): boolean {
|
||||
return Number.isSafeInteger(value);
|
||||
}
|
||||
|
||||
|
|
@ -900,7 +956,8 @@ export function isValidDate(value: unknown): value is Date {
|
|||
return isDate(value) && !isNan(Number(value));
|
||||
}
|
||||
|
||||
export function isValidLength(value: unknown): value is number {
|
||||
export function isValidLength<Input>(value: Input): value is NumericGuardResult<Input, ValidLength>;
|
||||
export function isValidLength(value: unknown): boolean {
|
||||
return isSafeInteger(value) && value >= 0;
|
||||
}
|
||||
|
||||
|
|
@ -956,6 +1013,8 @@ function typeErrorMessageMultipleValues(expectedType: AssertionTypeDescription |
|
|||
}
|
||||
|
||||
// Type assertions have to be declared with an explicit type.
|
||||
// Keep assertion outputs unbranded even when the corresponding `is.*` guard uses a branded subtype.
|
||||
// The brands exist to preserve useful false-branch narrowing for type guards on `number` inputs, which does not apply to `asserts`.
|
||||
type Assert = {
|
||||
// Unknowns.
|
||||
undefined: (value: unknown, message?: string) => asserts value is undefined;
|
||||
|
|
@ -1188,7 +1247,7 @@ const methodTypeMap = {
|
|||
isBigUint64Array: 'BigUint64Array',
|
||||
isBlob: 'Blob',
|
||||
isBoolean: 'boolean',
|
||||
isBoundFunction: 'Function',
|
||||
isBoundFunction: 'bound Function',
|
||||
isBuffer: 'Buffer',
|
||||
isClass: 'Class',
|
||||
isDataView: 'DataView',
|
||||
|
|
@ -1247,7 +1306,7 @@ const methodTypeMap = {
|
|||
isPromise: 'Promise',
|
||||
isPropertyKey: 'PropertyKey',
|
||||
isRegExp: 'RegExp',
|
||||
isSafeInteger: 'integer',
|
||||
isSafeInteger: 'safe integer',
|
||||
isSet: 'Set',
|
||||
isSharedArrayBuffer: 'SharedArrayBuffer',
|
||||
isString: 'string',
|
||||
|
|
@ -1391,7 +1450,7 @@ export function assertBoolean(value: unknown, message?: string): asserts value i
|
|||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
export function assertBoundFunction(value: unknown, message?: string): asserts value is Function {
|
||||
if (!isBoundFunction(value)) {
|
||||
throw new TypeError(message ?? typeErrorMessage('Function', value));
|
||||
throw new TypeError(message ?? typeErrorMessage('bound Function', value));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1752,7 +1811,7 @@ export function assertRegExp(value: unknown, message?: string): asserts value is
|
|||
|
||||
export function assertSafeInteger(value: unknown, message?: string): asserts value is number {
|
||||
if (!isSafeInteger(value)) {
|
||||
throw new TypeError(message ?? typeErrorMessage('integer', value));
|
||||
throw new TypeError(message ?? typeErrorMessage('safe integer', value));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1891,10 +1950,25 @@ export default is;
|
|||
export type {
|
||||
ArrayLike,
|
||||
Class,
|
||||
EvenInteger,
|
||||
FiniteNumber,
|
||||
Integer,
|
||||
NaN,
|
||||
NegativeInfinity,
|
||||
NegativeInteger,
|
||||
NegativeNumber,
|
||||
NodeStream,
|
||||
NonNegativeInteger,
|
||||
NonNegativeNumber,
|
||||
ObservableLike,
|
||||
OddInteger,
|
||||
PositiveInfinity,
|
||||
PositiveInteger,
|
||||
PositiveNumber,
|
||||
Predicate,
|
||||
Primitive,
|
||||
SafeInteger,
|
||||
TypedArray,
|
||||
UrlString,
|
||||
ValidLength,
|
||||
} from './types.ts';
|
||||
|
|
|
|||
113
source/types.ts
113
source/types.ts
|
|
@ -78,9 +78,122 @@ export type NonEmptyString = string & {0: string};
|
|||
|
||||
export type Whitespace = ' ';
|
||||
|
||||
type Brand<Key extends string> = Readonly<Record<Key, true>>;
|
||||
|
||||
/**
|
||||
A string that represents a valid URL.
|
||||
|
||||
This is a branded type to prevent incorrect TypeScript type narrowing.
|
||||
*/
|
||||
export type UrlString = string & {readonly __brand: 'UrlString'};
|
||||
|
||||
// Keep numeric guards branded and simple. This intentionally favors correct false-branch narrowing for `number` inputs over perfect success-branch narrowing for numeric literal unions.
|
||||
|
||||
/**
|
||||
The IEEE 754 "Not-a-Number" value, typed as a subtype of `number`.
|
||||
|
||||
Branded to prevent false-branch narrowing to `never` when the input is `number`.
|
||||
*/
|
||||
export type NaN = number & Brand<'__nanBrand'>;
|
||||
|
||||
/**
|
||||
A finite number (excludes `NaN`, `Infinity`, and `-Infinity`).
|
||||
|
||||
Branded to prevent false-branch narrowing to `never` when the input is `number`.
|
||||
*/
|
||||
export type FiniteNumber = number & Brand<'__finiteNumberBrand'>;
|
||||
|
||||
/**
|
||||
A number greater than or equal to zero.
|
||||
|
||||
Branded to prevent false-branch narrowing to `never` when the input is `number`.
|
||||
*/
|
||||
export type NonNegativeNumber = number & Brand<'__nonNegativeNumberBrand'>;
|
||||
|
||||
/**
|
||||
An integer value (no fractional part).
|
||||
|
||||
Branded to prevent false-branch narrowing to `never` when the input is `number`.
|
||||
*/
|
||||
export type Integer = FiniteNumber & Brand<'__integerBrand'>;
|
||||
|
||||
/**
|
||||
A number greater than zero.
|
||||
|
||||
Branded to prevent false-branch narrowing to `never` when the input is `number`.
|
||||
*/
|
||||
export type PositiveNumber = NonNegativeNumber & Brand<'__positiveNumberBrand'>;
|
||||
|
||||
/**
|
||||
A number less than zero.
|
||||
|
||||
Branded to prevent false-branch narrowing to `never` when the input is `number`.
|
||||
*/
|
||||
export type NegativeNumber = number & Brand<'__negativeNumberBrand'>;
|
||||
|
||||
/**
|
||||
An integer less than zero.
|
||||
|
||||
Branded to prevent false-branch narrowing to `never` when the input is `number`.
|
||||
*/
|
||||
export type NegativeInteger = Integer & NegativeNumber & Brand<'__negativeIntegerBrand'>;
|
||||
|
||||
/**
|
||||
An integer greater than or equal to zero.
|
||||
|
||||
Branded to prevent false-branch narrowing to `never` when the input is `number`.
|
||||
*/
|
||||
export type NonNegativeInteger = Integer & NonNegativeNumber & Brand<'__nonNegativeIntegerBrand'>;
|
||||
|
||||
/**
|
||||
An integer greater than zero.
|
||||
|
||||
Branded to prevent false-branch narrowing to `never` when the input is `number`.
|
||||
*/
|
||||
export type PositiveInteger = NonNegativeInteger & PositiveNumber & Brand<'__positiveIntegerBrand'>;
|
||||
|
||||
// Note: type-fest uses the `1e999` overflow trick to represent these types (since TypeScript has
|
||||
// no built-in Infinity type), but we use branded types here for consistency and to avoid
|
||||
// relying on numeric overflow behavior.
|
||||
|
||||
/**
|
||||
A positive infinite number (`Infinity`).
|
||||
|
||||
Branded to prevent false-branch narrowing to `never` when the input is `number`.
|
||||
*/
|
||||
export type PositiveInfinity = PositiveNumber & Brand<'__positiveInfinityBrand'>;
|
||||
|
||||
/**
|
||||
A negative infinite number (`-Infinity`).
|
||||
|
||||
Branded to prevent false-branch narrowing to `never` when the input is `number`.
|
||||
*/
|
||||
export type NegativeInfinity = NegativeNumber & Brand<'__negativeInfinityBrand'>;
|
||||
|
||||
/**
|
||||
A safe integer (within the range of `Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER`).
|
||||
|
||||
Branded to prevent false-branch narrowing to `never` when the input is `number`.
|
||||
*/
|
||||
export type SafeInteger = Integer & Brand<'__safeIntegerBrand'>;
|
||||
|
||||
/**
|
||||
An even integer.
|
||||
|
||||
Branded to prevent false-branch narrowing to `never` when the input is `number`.
|
||||
*/
|
||||
export type EvenInteger = Integer & Brand<'__evenIntegerBrand'>;
|
||||
|
||||
/**
|
||||
An odd integer.
|
||||
|
||||
Branded to prevent false-branch narrowing to `never` when the input is `number`.
|
||||
*/
|
||||
export type OddInteger = Integer & Brand<'__oddIntegerBrand'>;
|
||||
|
||||
/**
|
||||
A non-negative safe integer, suitable as an array or string length.
|
||||
|
||||
Branded to prevent false-branch narrowing to `never` when the input is `number`.
|
||||
*/
|
||||
export type ValidLength = SafeInteger & NonNegativeInteger & Brand<'__validLengthBrand'>;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue