Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IsInteger and IsFloat; Fix Integer and Float handing with edge case #857

Merged
merged 14 commits into from
Apr 22, 2024
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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.
sindresorhus marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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