Skip to content

Commit

Permalink
Add IsInteger and IsFloat, fix Integer and Float handing with…
Browse files Browse the repository at this point in the history
… edge case (#857)

Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
Emiyaaaaa and sindresorhus committed Apr 22, 2024
1 parent c1d2e0a commit f5b09de
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 9 deletions.
2 changes: 2 additions & 0 deletions index.d.ts
Expand Up @@ -98,6 +98,8 @@ export type {HasReadonlyKeys} from './source/has-readonly-keys';
export type {WritableKeysOf} from './source/writable-keys-of';
export type {HasWritableKeys} from './source/has-writable-keys';
export type {Spread} from './source/spread';
export type {IsInteger} from './source/is-integer';
export type {IsFloat} from './source/is-float';
export type {TupleToUnion} from './source/tuple-to-union';
export type {IntRange} from './source/int-range';
export type {IsEqual} from './source/is-equal';
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Expand Up @@ -285,6 +285,8 @@ type ShouldBeNever = IfAny<'not any', 'not never', 'never'>;
- [`NegativeInteger`](source/numeric.d.ts) - A negative (`-∞ < x < 0`) `number` that is an integer.
- [`NonNegativeInteger`](source/numeric.d.ts) - A non-negative (`0 <= x < ∞`) `number` that is an integer.
- [`IsNegative`](source/numeric.d.ts) - Returns a boolean for whether the given number is a negative number.
- [`IsFloat`](source/is-float.d.ts) - Returns a boolean for whether the given number is a float, like `1.5` or `-1.5`.
- [`IsInteger`](source/is-integer.d.ts) - Returns a boolean for whether the given number is a integer, like `-5`, `1.0` or `100`.
- [`GreaterThan`](source/greater-than.d.ts) - Returns a boolean for whether a given number is greater than another number.
- [`GreaterThanOrEqual`](source/greater-than-or-equal.d.ts) - Returns a boolean for whether a given number is greater than or equal to another number.
- [`LessThan`](source/less-than.d.ts) - Returns a boolean for whether a given number is less than another number.
Expand Down
33 changes: 33 additions & 0 deletions source/is-float.d.ts
@@ -0,0 +1,33 @@
import type {Zero} from './numeric';

/**
Returns a boolean for whether the given number is a float, like `1.5` or `-1.5`.
It returns `false` for `Infinity`.
Use-case:
- If you want to make a conditional branch based on the result of whether a number is a float or not.
@example
```
type Float = IsFloat<1.5>;
//=> true
type IntegerWithDecimal = IsInteger<1.0>;
//=> false
type NegativeFloat = IsInteger<-1.5>;
//=> true
type Infinity_ = IsInteger<Infinity>;
//=> false
```
*/
export type IsFloat<T> =
T extends number
? `${T}` extends `${infer _Sign extends '' | '-'}${number}.${infer Decimal extends number}`
? Decimal extends Zero
? false
: true
: false
: false;
48 changes: 48 additions & 0 deletions source/is-integer.d.ts
@@ -0,0 +1,48 @@
import type {Not} from './internal';
import type {IsFloat} from './is-float';
import type {PositiveInfinity, NegativeInfinity} from './numeric';

/**
Returns a boolean for whether the given number is a integer, like `-5`, `1.0` or `100`.
Like [`Number#IsInteger()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/IsInteger) but for types.
Use-case:
- If you want to make a conditional branch based on the result of whether a number is a intrger or not.
@example
```
type Integer = IsInteger<1>;
//=> true
type IntegerWithDecimal = IsInteger<1.0>;
//=> true
type NegativeInteger = IsInteger<-1>;
//=> true
type Float = IsInteger<1.5>;
//=> false
// Supports non-decimal numbers
type OctalInteger: IsInteger<0o10>;
//=> true
type BinaryInteger: IsInteger<0b10>;
//=> true
type HexadecimalInteger: IsInteger<0x10>;
//=> true
```
*/
export type IsInteger<T> =
T extends bigint
? true
: T extends number
? number extends T
? false
: T extends PositiveInfinity | NegativeInfinity
? false
: Not<IsFloat<T>>
: false;
43 changes: 39 additions & 4 deletions source/numeric.d.ts
@@ -1,3 +1,6 @@
import type {IsFloat} from './is-float';
import type {IsInteger} from './is-integer';

export type Numeric = number | bigint;

type Zero = 0 | 0n;
Expand Down Expand Up @@ -49,10 +52,35 @@ export type Finite<T extends number> = T extends PositiveInfinity | NegativeInfi

/**
A `number` that is an integer.
You can't pass a `bigint` as they are already guaranteed to be integers.
Use-case: Validating and documenting parameters.
@example
```
type Integer = Integer<1>;
//=> 1
type IntegerWithDecimal = Integer<1.0>;
//=> 1
type NegativeInteger = Integer<-1>;
//=> -1
type Float = Integer<1.5>;
//=> never
// Supports non-decimal numbers
type OctalInteger: Integer<0o10>;
//=> 0o10
type BinaryInteger: Integer<0b10>;
//=> 0b10
type HexadecimalInteger: Integer<0x10>;
//=> 0x10
```
@example
```
import type {Integer} from 'type-fest';
Expand All @@ -67,14 +95,18 @@ declare function setYear<T extends number>(length: Integer<T>): void;
*/
// `${bigint}` is a type that matches a valid bigint literal without the `n` (ex. 1, 0b1, 0o1, 0x1)
// Because T is a number and not a string we can effectively use this to filter out any numbers containing decimal points
export type Integer<T extends number> = `${T}` extends `${bigint}` ? T : never;
export type Integer<T> =
T extends unknown // To distributive type
? IsInteger<T> extends true ? T : never
: never; // Never happens

/**
A `number` that is not an integer.
You can't pass a `bigint` as they are already guaranteed to be integers.
Use-case: Validating and documenting parameters.
It does not accept `Infinity`.
@example
```
import type {Float} from 'type-fest';
Expand All @@ -86,7 +118,10 @@ declare function setPercentage<T extends number>(length: Float<T>): void;
@category Numeric
*/
export type Float<T extends number> = T extends Integer<T> ? never : T;
export type Float<T> =
T extends unknown // To distributive type
? IsFloat<T> extends true ? T : never
: never; // Never happens

/**
A negative (`-∞ < x < 0`) `number` that is not an integer.
Expand Down
17 changes: 17 additions & 0 deletions test-d/is-float.ts
@@ -0,0 +1,17 @@
import {expectType} from 'tsd';
import type {IsFloat, PositiveInfinity} from '../index';

expectType<false>({} as IsFloat<0>);
expectType<false>({} as IsFloat<1>);
expectType<false>({} as IsFloat<1.0>); // eslint-disable-line unicorn/no-zero-fractions
expectType<true>({} as IsFloat<1.5>);
expectType<false>({} as IsFloat<-1>);
expectType<false>({} as IsFloat<number>);
expectType<false>({} as IsFloat<0o10>);
expectType<false>({} as IsFloat<1n>);
expectType<false>({} as IsFloat<0n>);
expectType<false>({} as IsFloat<0b10>);
expectType<false>({} as IsFloat<0x10>);
expectType<false>({} as IsFloat<1e+100>);
expectType<false>({} as IsFloat<PositiveInfinity>);
expectType<false>({} as IsFloat<typeof Number.POSITIVE_INFINITY>);
17 changes: 17 additions & 0 deletions test-d/is-integer.ts
@@ -0,0 +1,17 @@
import {expectType} from 'tsd';
import type {IsInteger, PositiveInfinity} from '../index';

expectType<true>({} as IsInteger<0>);
expectType<true>({} as IsInteger<1>);
expectType<true>({} as IsInteger<1.0>); // eslint-disable-line unicorn/no-zero-fractions
expectType<false>({} as IsInteger<1.5>);
expectType<true>({} as IsInteger<-1>);
expectType<false>({} as IsInteger<number>);
expectType<true>({} as IsInteger<0o10>);
expectType<true>({} as IsInteger<1n>);
expectType<true>({} as IsInteger<0n>);
expectType<true>({} as IsInteger<0b10>);
expectType<true>({} as IsInteger<0x10>);
expectType<true>({} as IsInteger<1e+100>);
expectType<false>({} as IsInteger<PositiveInfinity>);
expectType<false>({} as IsInteger<typeof Number.POSITIVE_INFINITY>);
25 changes: 20 additions & 5 deletions test-d/numeric.ts
Expand Up @@ -21,25 +21,40 @@ expectType<1>(infinityMixed);

// Integer
declare const integer: Integer<1>;
declare const integerMixed: Integer<1 | 1.5>;
declare const integerWithDecimal: Integer<1.0>; // eslint-disable-line unicorn/no-zero-fractions
declare const numberType: Integer<number>;
declare const integerMixed: Integer<1 | 1.5 | -1>;
declare const bigInteger: Integer<1e+100>;
declare const octalInteger: Integer<0o10>;
declare const binaryInteger: Integer<0b10>;
declare const hexadecimalInteger: Integer<0x10>;
declare const nonInteger: Integer<1.5>;
declare const infinityInteger: Integer<PositiveInfinity | NegativeInfinity>;
const infinityValue = Number.POSITIVE_INFINITY;
declare const infinityInteger2: Integer<typeof infinityValue>;

expectType<1>(integer);
expectType<never>(integerMixed); // This may be undesired behavior
expectType<1>(integerWithDecimal);
expectType<never>(numberType);
expectType<1 | -1>(integerMixed);
expectType<1e+100>(bigInteger);
expectType<0o10>(octalInteger);
expectType<0b10>(binaryInteger);
expectType<0x10>(hexadecimalInteger);
expectType<never>(nonInteger);
expectType<never>(infinityInteger);
expectType<never>(infinityInteger2);

// Float
declare const float: Float<1.5>;
declare const floatMixed: Float<1 | 1.5>;
declare const floatMixed: Float<1 | 1.5 | -1.5>;
declare const nonFloat: Float<1>;
declare const infinityFloat: Float<PositiveInfinity | NegativeInfinity>;

expectType<1.5>(float);
expectType<1.5>(floatMixed);
expectType<1.5 | -1.5>(floatMixed);
expectType<never>(nonFloat);
expectType<PositiveInfinity | NegativeInfinity>(infinityFloat); // According to Number.isInteger
expectType<never>(infinityFloat);

// Negative
declare const negative: Negative<-1 | -1n | 0 | 0n | 1 | 1n>;
Expand Down

0 comments on commit f5b09de

Please sign in to comment.