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
74 changes: 72 additions & 2 deletions source/numeric.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,51 @@
import type {Not} from './internal';

export type Numeric = number | bigint;

type Zero = 0 | 0n;

/**
Returns the given number if it is a float, like `1.5` or `-1.5`.
sindresorhus marked this conversation as resolved.
Show resolved Hide resolved
*/
type IsFloat<T extends number> =
`${T}` extends `${infer _Sign extends '' | '-'}${number}.${infer Decimal extends number}`
? Decimal extends Zero
? false
: true
: false;

/**
Returns the given number if it is an integer, like `-5`, `1` or `100`.
sindresorhus marked this conversation as resolved.
Show resolved Hide resolved

Like [`Number#IsInteger()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/IsInteger) but for types.

@example
```
type Integer = IsInteger<1>;
//=> true
type IntegerWithDecimal = IsInteger<1.0>;
//=> true
type NegativeInteger = IsInteger<-1>;
//=> true
type Float = IsInteger<1.5>;
//=> false

//=> supported non-decimal numbers
type OctalInteger: IsInteger<0o10>;
//=> true
type BinaryInteger: IsInteger<0b10>;
//=> true
type HexadecimalInteger: IsInteger<0x10>;
//=> true
```
*/
type IsInteger<T extends number> =
number extends T
? false
: T extends PositiveInfinity | NegativeInfinity
? false
: Not<IsFloat<T>>;

/**
Matches the hidden `Infinity` type.

Expand Down Expand Up @@ -53,6 +97,26 @@ 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

//=> supported 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,7 +131,10 @@ 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 extends number> =
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
Expand All @@ -86,7 +153,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 extends number> =
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
22 changes: 17 additions & 5 deletions test-d/numeric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,37 @@ 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>;

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);

// 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