Compare commits
20 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7821031c66 | ||
|
|
a439305554 | ||
|
|
2d4956e634 | ||
|
|
48df5c429c | ||
|
|
13febb6b01 | ||
|
|
cb4ee0e92c | ||
|
|
54fc09406a | ||
|
|
63be5c0c19 | ||
|
|
ac46b5400d | ||
|
|
47415dc46a | ||
|
|
3b40955b02 | ||
|
|
faf700367e | ||
|
|
eff8e6b318 | ||
|
|
9bdcd9b57f | ||
|
|
fbcc68e139 | ||
|
|
e7c84fcb79 | ||
|
|
d22ab62991 | ||
|
|
1f2440ae0d | ||
|
|
c68ad76062 | ||
|
|
ef35cc350a |
12 changed files with 3069 additions and 1747 deletions
7
.github/workflows/main.yml
vendored
7
.github/workflows/main.yml
vendored
|
|
@ -10,10 +10,11 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
node-version:
|
node-version:
|
||||||
- 20
|
- 24
|
||||||
|
- 22
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- run: npm install
|
- run: npm install
|
||||||
|
|
|
||||||
9
AGENTS.md
Normal file
9
AGENTS.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Notes
|
||||||
|
|
||||||
|
## Branded types for type guards
|
||||||
|
|
||||||
|
TypeScript type guards narrow in both branches. If `is.integer(n)` returns `value is number` and the input is `number`, the false branch computes `Exclude<number, number>` = `never`. This makes common patterns like `if (!is.integer(n)) throw; use(n)` fail because `n` becomes `never` after the guard.
|
||||||
|
|
||||||
|
To avoid this, type guard predicates use branded types (e.g., `number & {readonly __brand: 'Integer'}`, `string & {readonly __brand: 'UrlString'}`). A branded subtype ensures the false branch stays the original type (e.g., `Exclude<number, Integer>` = `number`).
|
||||||
|
|
||||||
|
Assert functions (`asserts value is T`) don't need branded types since they throw on failure and have no false branch. They use plain types like `asserts value is number`.
|
||||||
1
CLAUDE.md
Symbolic link
1
CLAUDE.md
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
AGENTS.md
|
||||||
51
package.json
51
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@sindresorhus/is",
|
"name": "@sindresorhus/is",
|
||||||
"version": "7.0.2",
|
"version": "8.1.0",
|
||||||
"description": "Type check values",
|
"description": "Type check values",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "sindresorhus/is",
|
"repository": "sindresorhus/is",
|
||||||
|
|
@ -17,11 +17,11 @@
|
||||||
},
|
},
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=22"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "del distribution && tsc",
|
"build": "del distribution && tsc",
|
||||||
"test": "tsc --noEmit && xo && ava",
|
"test": "tsc --noEmit && tsc --project test/tsconfig.json --noEmit && xo && node --experimental-transform-types --test test/test.ts",
|
||||||
"prepare": "npm run build"
|
"prepare": "npm run build"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|
@ -51,31 +51,26 @@
|
||||||
"typeguards",
|
"typeguards",
|
||||||
"types"
|
"types"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"xo": {
|
||||||
"@sindresorhus/tsconfig": "^6.0.0",
|
"rules": {
|
||||||
"@types/jsdom": "^21.1.7",
|
"@typescript-eslint/no-unsafe-enum-comparison": "off",
|
||||||
"@types/node": "^20.14.10",
|
"@typescript-eslint/no-confusing-void-expression": "off",
|
||||||
"@types/zen-observable": "^0.8.7",
|
"@typescript-eslint/no-unsafe-type-assertion": "off",
|
||||||
"ava": "^6.1.3",
|
"@stylistic/operator-linebreak": "off"
|
||||||
"del-cli": "^5.1.0",
|
}
|
||||||
"expect-type": "^0.19.0",
|
|
||||||
"jsdom": "^24.1.0",
|
|
||||||
"rxjs": "^7.8.1",
|
|
||||||
"tempy": "^3.1.0",
|
|
||||||
"tsimp": "2.0.11",
|
|
||||||
"typescript": "5.5.3",
|
|
||||||
"xo": "^0.58.0",
|
|
||||||
"zen-observable": "^0.10.0"
|
|
||||||
},
|
},
|
||||||
"ava": {
|
"devDependencies": {
|
||||||
"environmentVariables": {
|
"@sindresorhus/tsconfig": "^8.1.0",
|
||||||
"TSIMP_DIAG": "error"
|
"@types/jsdom": "^28.0.1",
|
||||||
},
|
"@types/node": "^25.5.2",
|
||||||
"extensions": {
|
"@types/zen-observable": "^0.8.7",
|
||||||
"ts": "module"
|
"del-cli": "^7.0.0",
|
||||||
},
|
"expect-type": "^1.3.0",
|
||||||
"nodeArguments": [
|
"jsdom": "^29.0.2",
|
||||||
"--import=tsimp/import"
|
"rxjs": "^7.8.2",
|
||||||
]
|
"tempy": "^3.2.0",
|
||||||
|
"typescript": "6.0.2",
|
||||||
|
"xo": "^2.0.2",
|
||||||
|
"zen-observable": "^0.10.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
159
readme.md
159
readme.md
|
|
@ -130,6 +130,17 @@ is.array(value); // Validate `value` is an array.
|
||||||
is.array(value, is.number); // Validate `value` is an array and all of its items are numbers.
|
is.array(value, is.number); // Validate `value` is an array and all of its items are numbers.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### .arrayOf(predicate)
|
||||||
|
|
||||||
|
Returns a type guard that checks if `value` is an array where every item matches the predicate. Useful for composing with other methods.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const isStringArray = is.arrayOf(is.string);
|
||||||
|
|
||||||
|
isStringArray(['a', 'b']); //=> true
|
||||||
|
isStringArray(['a', 1]); //=> false
|
||||||
|
```
|
||||||
|
|
||||||
##### .function(value)
|
##### .function(value)
|
||||||
|
|
||||||
##### .buffer(value)
|
##### .buffer(value)
|
||||||
|
|
@ -463,6 +474,10 @@ function foo() {
|
||||||
foo();
|
foo();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### .finiteNumber(value)
|
||||||
|
|
||||||
|
Check if `value` is a number and is finite. Excludes `Infinity` and `-Infinity`.
|
||||||
|
|
||||||
##### .positiveNumber(value)
|
##### .positiveNumber(value)
|
||||||
|
|
||||||
Check if `value` is a number and is more than 0.
|
Check if `value` is a number and is more than 0.
|
||||||
|
|
@ -471,6 +486,22 @@ Check if `value` is a number and is more than 0.
|
||||||
|
|
||||||
Check if `value` is a number and is less than 0.
|
Check if `value` is a number and is less than 0.
|
||||||
|
|
||||||
|
##### .nonNegativeNumber(value)
|
||||||
|
|
||||||
|
Check if `value` is a number and is 0 or more.
|
||||||
|
|
||||||
|
##### .positiveInteger(value)
|
||||||
|
|
||||||
|
Check if `value` is an integer and is more than 0.
|
||||||
|
|
||||||
|
##### .negativeInteger(value)
|
||||||
|
|
||||||
|
Check if `value` is an integer and is less than 0.
|
||||||
|
|
||||||
|
##### .nonNegativeInteger(value)
|
||||||
|
|
||||||
|
Check if `value` is an integer and is 0 or more.
|
||||||
|
|
||||||
##### .inRange(value, range)
|
##### .inRange(value, range)
|
||||||
|
|
||||||
Check if `value` (number) is in the given `range`. The range is an array of two values, lower bound and upper bound, in no specific order.
|
Check if `value` (number) is in the given `range`. The range is an array of two values, lower bound and upper bound, in no specific order.
|
||||||
|
|
@ -575,6 +606,31 @@ is.any([is.boolean, is.number], 'unicorns', [], new Map());
|
||||||
//=> false
|
//=> false
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### .any(predicate[])
|
||||||
|
|
||||||
|
Using an array of `predicate[]` without values, returns a combined type guard that checks if a value matches **any** of the predicates:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const isStringOrNumber = is.any([is.string, is.number]);
|
||||||
|
|
||||||
|
isStringOrNumber('hello');
|
||||||
|
//=> true
|
||||||
|
|
||||||
|
isStringOrNumber(123);
|
||||||
|
//=> true
|
||||||
|
|
||||||
|
isStringOrNumber(true);
|
||||||
|
//=> false
|
||||||
|
```
|
||||||
|
|
||||||
|
This is useful for composing with other methods like `is.optional`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
is.optional(value, is.any([is.string, is.number]));
|
||||||
|
```
|
||||||
|
|
||||||
|
An empty predicate array currently returns a predicate that always returns `false`. This will throw in the next major release.
|
||||||
|
|
||||||
##### .all(predicate, ...values)
|
##### .all(predicate, ...values)
|
||||||
|
|
||||||
Returns `true` if **all** of the input `values` returns true in the `predicate`:
|
Returns `true` if **all** of the input `values` returns true in the `predicate`:
|
||||||
|
|
@ -587,6 +643,54 @@ is.all(is.string, '🦄', [], 'unicorns');
|
||||||
//=> false
|
//=> false
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### .all(predicate[])
|
||||||
|
|
||||||
|
Using an array of `predicate[]` without values, returns a combined type guard that checks if a value matches **all** of the predicates:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const isArrayAndNonEmpty = is.all([is.array, is.nonEmptyArray]);
|
||||||
|
|
||||||
|
isArrayAndNonEmpty(['hello']);
|
||||||
|
//=> true
|
||||||
|
|
||||||
|
isArrayAndNonEmpty([]);
|
||||||
|
//=> false
|
||||||
|
```
|
||||||
|
|
||||||
|
This is useful for composing with other methods like `is.optional`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
is.optional(value, is.all([is.object, is.plainObject]));
|
||||||
|
```
|
||||||
|
|
||||||
|
An empty predicate array currently returns a predicate that always returns `true`. This will throw in the next major release.
|
||||||
|
|
||||||
|
##### .optional(value, predicate)
|
||||||
|
|
||||||
|
Returns `true` if `value` is `undefined` or satisfies the given `predicate`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
is.optional(undefined, is.string);
|
||||||
|
//=> true
|
||||||
|
|
||||||
|
is.optional('🦄', is.string);
|
||||||
|
//=> true
|
||||||
|
|
||||||
|
is.optional(123, is.string);
|
||||||
|
//=> false
|
||||||
|
```
|
||||||
|
|
||||||
|
##### .oneOf(values)
|
||||||
|
|
||||||
|
Returns a type guard that checks if `value` is one of the given `values`. Best used with `as const` for precise type narrowing.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const isDirection = is.oneOf(['north', 'south', 'east', 'west'] as const);
|
||||||
|
|
||||||
|
isDirection('north'); //=> true
|
||||||
|
isDirection('up'); //=> false
|
||||||
|
```
|
||||||
|
|
||||||
##### .validDate(value)
|
##### .validDate(value)
|
||||||
|
|
||||||
Returns `true` if the value is a valid date.
|
Returns `true` if the value is a valid date.
|
||||||
|
|
@ -682,6 +786,61 @@ handleMovieRatingApiResponse({rating: 0.87, title: 'The Matrix'});
|
||||||
handleMovieRatingApiResponse({rating: '🦄'});
|
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`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import {assert} from '@sindresorhus/is';
|
||||||
|
|
||||||
|
assert.optional(undefined, assert.string);
|
||||||
|
// Passes without throwing
|
||||||
|
|
||||||
|
assert.optional('🦄', assert.string);
|
||||||
|
// Passes without throwing
|
||||||
|
|
||||||
|
assert.optional(123, assert.string);
|
||||||
|
// Throws: Expected value which is `string`, received value of type `number`
|
||||||
|
```
|
||||||
|
|
||||||
## Generic type parameters
|
## Generic type parameters
|
||||||
|
|
||||||
The type guards and type assertions are aware of [generic type parameters](https://www.typescriptlang.org/docs/handbook/generics.html), such as `Promise<T>` and `Map<Key, Value>`. The default is `unknown` for most cases, since `is` cannot check them at runtime. If the generic type is known at compile-time, either implicitly (inferred) or explicitly (provided), `is` propagates the type so it can be used later.
|
The type guards and type assertions are aware of [generic type parameters](https://www.typescriptlang.org/docs/handbook/generics.html), such as `Promise<T>` and `Map<Key, Value>`. The default is `unknown` for most cases, since `is` cannot check them at runtime. If the generic type is known at compile-time, either implicitly (inferred) or explicitly (provided), `is` propagates the type so it can be used later.
|
||||||
|
|
|
||||||
507
source/index.ts
507
source/index.ts
File diff suppressed because it is too large
Load diff
128
source/types.ts
128
source/types.ts
|
|
@ -4,7 +4,8 @@
|
||||||
Matches any [primitive value](https://developer.mozilla.org/en-US/docs/Glossary/Primitive).
|
Matches any [primitive value](https://developer.mozilla.org/en-US/docs/Glossary/Primitive).
|
||||||
*/
|
*/
|
||||||
export type Primitive =
|
export type Primitive =
|
||||||
| null // eslint-disable-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/no-restricted-types
|
||||||
|
| null
|
||||||
| undefined
|
| undefined
|
||||||
| string
|
| string
|
||||||
| number
|
| number
|
||||||
|
|
@ -53,10 +54,11 @@ export type ObservableLike = {
|
||||||
[Symbol.observable](): ObservableLike;
|
[Symbol.observable](): ObservableLike;
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/no-restricted-types
|
||||||
export type Falsy = false | 0 | 0n | '' | null | undefined;
|
export type Falsy = false | 0 | 0n | '' | null | undefined;
|
||||||
|
|
||||||
export type WeakRef<T extends object> = { // eslint-disable-line @typescript-eslint/ban-types, unicorn/prevent-abbreviations
|
// eslint-disable-next-line @typescript-eslint/no-restricted-types
|
||||||
|
export type WeakRef<T extends object> = {
|
||||||
readonly [Symbol.toStringTag]: 'WeakRef';
|
readonly [Symbol.toStringTag]: 'WeakRef';
|
||||||
deref(): T | undefined;
|
deref(): T | undefined;
|
||||||
};
|
};
|
||||||
|
|
@ -75,3 +77,123 @@ export type Predicate = (value: unknown) => boolean;
|
||||||
export type NonEmptyString = string & {0: string};
|
export type NonEmptyString = string & {0: string};
|
||||||
|
|
||||||
export type Whitespace = ' ';
|
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'>;
|
||||||
|
|
|
||||||
3
source/utilities.ts
Normal file
3
source/utilities.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function keysOf<T extends Record<PropertyKey, unknown>>(value: T): Array<keyof T> {
|
||||||
|
return Object.keys(value) as Array<keyof T>; // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
|
||||||
|
}
|
||||||
3528
test/test.ts
3528
test/test.ts
File diff suppressed because it is too large
Load diff
12
test/tsconfig.json
Normal file
12
test/tsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "..",
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"../source",
|
||||||
|
"type-tests.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
405
test/type-tests.ts
Normal file
405
test/type-tests.ts
Normal file
|
|
@ -0,0 +1,405 @@
|
||||||
|
import {expectTypeOf} from 'expect-type';
|
||||||
|
import is, {
|
||||||
|
assert as isAssert,
|
||||||
|
assertNotNullOrUndefined,
|
||||||
|
assertNotPrimitive,
|
||||||
|
assertNotString,
|
||||||
|
assertNotUndefined,
|
||||||
|
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 Primitive,
|
||||||
|
type SafeInteger,
|
||||||
|
type ValidLength,
|
||||||
|
} from '../source/index.ts';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-restricted-types
|
||||||
|
type UnknownNotPrimitive<Forbidden extends Primitive> = Exclude<Primitive, Forbidden> | 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`).
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNotUndefinedCheck = (value: string | undefined) => {
|
||||||
|
isAssert.not.undefined(value);
|
||||||
|
expectTypeOf(value).toEqualTypeOf<string>();
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNotUndefinedUnknownCheck = (value: unknown) => {
|
||||||
|
isAssert.not.undefined(value);
|
||||||
|
expectTypeOf(value).toEqualTypeOf<UnknownNotPrimitive<undefined>>();
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNotUndefinedGenericCheck = <T>(value: T) => {
|
||||||
|
isAssert.not.undefined(value);
|
||||||
|
const _: Exclude<T, undefined> = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const nullValue = null;
|
||||||
|
type Null = typeof nullValue;
|
||||||
|
|
||||||
|
const assertNotNullUnknownCheck = (value: unknown) => {
|
||||||
|
isAssert.not.null(value);
|
||||||
|
expectTypeOf(value).toEqualTypeOf<UnknownNotPrimitive<Null>>();
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNotNullOrUndefinedCheck = (value: string | Null | undefined) => {
|
||||||
|
isAssert.not.nullOrUndefined(value);
|
||||||
|
expectTypeOf(value).toEqualTypeOf<string>();
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNotNullOrUndefinedUnknownCheck = (value: unknown) => {
|
||||||
|
isAssert.not.nullOrUndefined(value);
|
||||||
|
expectTypeOf(value).toEqualTypeOf<UnknownNotPrimitive<Null | undefined>>();
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNotStringCheck = (value: string | number) => {
|
||||||
|
isAssert.not.string(value);
|
||||||
|
expectTypeOf(value).toEqualTypeOf<number>();
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNotStringUnknownCheck = (value: unknown) => {
|
||||||
|
isAssert.not.string(value);
|
||||||
|
expectTypeOf(value).toEqualTypeOf<UnknownNotPrimitive<string>>();
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNotStringGenericCheck = <T>(value: T) => {
|
||||||
|
isAssert.not.string(value);
|
||||||
|
const _: Exclude<T, string> = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNotBooleanUnknownCheck = (value: unknown) => {
|
||||||
|
isAssert.not.boolean(value);
|
||||||
|
expectTypeOf(value).toEqualTypeOf<UnknownNotPrimitive<boolean>>();
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNotSymbolUnknownCheck = (value: unknown) => {
|
||||||
|
isAssert.not.symbol(value);
|
||||||
|
expectTypeOf(value).toEqualTypeOf<UnknownNotPrimitive<symbol>>();
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNotBigintUnknownCheck = (value: unknown) => {
|
||||||
|
isAssert.not.bigint(value);
|
||||||
|
expectTypeOf(value).toEqualTypeOf<UnknownNotPrimitive<bigint>>();
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNotPrimitiveUnknownCheck = (value: unknown) => {
|
||||||
|
isAssert.not.primitive(value);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-restricted-types
|
||||||
|
expectTypeOf(value).toEqualTypeOf<object>();
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNotPrimitiveGenericCheck = <T>(value: T) => {
|
||||||
|
isAssert.not.primitive(value);
|
||||||
|
const _: Exclude<T, Primitive> = 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<UnknownNotPrimitive<Null | undefined>>();
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNotNamedStringExportCheck = (value: string | number) => {
|
||||||
|
assertNotString(value);
|
||||||
|
expectTypeOf(value).toEqualTypeOf<number>();
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNotNamedStringUnknownExportCheck = (value: unknown) => {
|
||||||
|
assertNotString(value);
|
||||||
|
expectTypeOf(value).toEqualTypeOf<UnknownNotPrimitive<string>>();
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNotNamedPrimitiveUnknownExportCheck = (value: unknown) => {
|
||||||
|
assertNotPrimitive(value);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-restricted-types
|
||||||
|
expectTypeOf(value).toEqualTypeOf<object>();
|
||||||
|
};
|
||||||
|
|
||||||
|
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, unknown> | 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, unknown> | 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, number> | 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, number> | string = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertNotSetDoesNotExistCheck = (value: Set<string> | 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> | 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);
|
||||||
|
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);
|
||||||
|
assertNotUndefinedCheck('🦄');
|
||||||
|
assertNotUndefinedUnknownCheck('🦄');
|
||||||
|
assertNotUndefinedGenericCheck<string | undefined>('🦄');
|
||||||
|
assertNotNullUnknownCheck('🦄');
|
||||||
|
assertNotNullOrUndefinedCheck('🦄');
|
||||||
|
assertNotNullOrUndefinedUnknownCheck('🦄');
|
||||||
|
assertNotStringCheck(1);
|
||||||
|
assertNotStringUnknownCheck(1);
|
||||||
|
assertNotStringGenericCheck<string | number>(1);
|
||||||
|
assertNotBooleanUnknownCheck(1);
|
||||||
|
assertNotSymbolUnknownCheck(1);
|
||||||
|
assertNotBigintUnknownCheck(1);
|
||||||
|
assertNotPrimitiveUnknownCheck({});
|
||||||
|
assertNotPrimitiveGenericCheck<string | {unicorn: true}>({unicorn: true});
|
||||||
|
assertNotNamedUndefinedExportCheck(0);
|
||||||
|
assertNotNamedNullOrUndefinedUnknownExportCheck('🦄');
|
||||||
|
assertNotNamedStringExportCheck(1);
|
||||||
|
assertNotNamedStringUnknownExportCheck(1);
|
||||||
|
assertNotNamedPrimitiveUnknownExportCheck({});
|
||||||
|
assertNotCallableDoesNotExistCheck('🦄');
|
||||||
|
assertNotNumberDoesNotExistCheck(Number.NaN);
|
||||||
|
assertNotIntegerDoesNotExistCheck(1.5);
|
||||||
|
assertNotObjectDoesNotExistCheck('🦄');
|
||||||
|
assertNotBlobDoesNotExistCheck('🦄');
|
||||||
|
assertNotMapDoesNotExistCheck('🦄');
|
||||||
|
assertNotSetDoesNotExistCheck('🦄');
|
||||||
|
assertNotDateDoesNotExistCheck('🦄');
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
{
|
{
|
||||||
"extends": "@sindresorhus/tsconfig",
|
"extends": "@sindresorhus/tsconfig",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["node"],
|
||||||
|
"rootDir": "source",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"rewriteRelativeImportExtensions": true
|
||||||
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"source"
|
"source"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue