diff --git a/index.d.ts b/index.d.ts index 5c62cd00..af61223c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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'; diff --git a/readme.md b/readme.md index 443d15aa..9f44ac63 100644 --- a/readme.md +++ b/readme.md @@ -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. diff --git a/source/is-float.d.ts b/source/is-float.d.ts new file mode 100644 index 00000000..dd3c2723 --- /dev/null +++ b/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; +//=> false +``` +*/ +export type IsFloat = +T extends number + ? `${T}` extends `${infer _Sign extends '' | '-'}${number}.${infer Decimal extends number}` + ? Decimal extends Zero + ? false + : true + : false + : false; diff --git a/source/is-integer.d.ts b/source/is-integer.d.ts new file mode 100644 index 00000000..56ba901f --- /dev/null +++ b/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 extends bigint + ? true + : T extends number + ? number extends T + ? false + : T extends PositiveInfinity | NegativeInfinity + ? false + : Not> + : false; diff --git a/source/numeric.d.ts b/source/numeric.d.ts index e6906dd1..3edbf8db 100644 --- a/source/numeric.d.ts +++ b/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; @@ -49,10 +52,35 @@ export type Finite = 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'; @@ -67,14 +95,18 @@ declare function setYear(length: Integer): 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 `${bigint}` ? T : never; +export type Integer = + T extends unknown // To distributive type + ? IsInteger 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'; @@ -86,7 +118,10 @@ declare function setPercentage(length: Float): void; @category Numeric */ -export type Float = T extends Integer ? never : T; +export type Float = +T extends unknown // To distributive type + ? IsFloat extends true ? T : never + : never; // Never happens /** A negative (`-∞ < x < 0`) `number` that is not an integer. diff --git a/test-d/is-float.ts b/test-d/is-float.ts new file mode 100644 index 00000000..0ec1cbca --- /dev/null +++ b/test-d/is-float.ts @@ -0,0 +1,17 @@ +import {expectType} from 'tsd'; +import type {IsFloat, PositiveInfinity} from '../index'; + +expectType({} as IsFloat<0>); +expectType({} as IsFloat<1>); +expectType({} as IsFloat<1.0>); // eslint-disable-line unicorn/no-zero-fractions +expectType({} as IsFloat<1.5>); +expectType({} as IsFloat<-1>); +expectType({} as IsFloat); +expectType({} as IsFloat<0o10>); +expectType({} as IsFloat<1n>); +expectType({} as IsFloat<0n>); +expectType({} as IsFloat<0b10>); +expectType({} as IsFloat<0x10>); +expectType({} as IsFloat<1e+100>); +expectType({} as IsFloat); +expectType({} as IsFloat); diff --git a/test-d/is-integer.ts b/test-d/is-integer.ts new file mode 100644 index 00000000..968aadb1 --- /dev/null +++ b/test-d/is-integer.ts @@ -0,0 +1,17 @@ +import {expectType} from 'tsd'; +import type {IsInteger, PositiveInfinity} from '../index'; + +expectType({} as IsInteger<0>); +expectType({} as IsInteger<1>); +expectType({} as IsInteger<1.0>); // eslint-disable-line unicorn/no-zero-fractions +expectType({} as IsInteger<1.5>); +expectType({} as IsInteger<-1>); +expectType({} as IsInteger); +expectType({} as IsInteger<0o10>); +expectType({} as IsInteger<1n>); +expectType({} as IsInteger<0n>); +expectType({} as IsInteger<0b10>); +expectType({} as IsInteger<0x10>); +expectType({} as IsInteger<1e+100>); +expectType({} as IsInteger); +expectType({} as IsInteger); diff --git a/test-d/numeric.ts b/test-d/numeric.ts index 75466f5a..b8718b44 100644 --- a/test-d/numeric.ts +++ b/test-d/numeric.ts @@ -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; +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; +const infinityValue = Number.POSITIVE_INFINITY; +declare const infinityInteger2: Integer; expectType<1>(integer); -expectType(integerMixed); // This may be undesired behavior +expectType<1>(integerWithDecimal); +expectType(numberType); +expectType<1 | -1>(integerMixed); +expectType<1e+100>(bigInteger); +expectType<0o10>(octalInteger); +expectType<0b10>(binaryInteger); +expectType<0x10>(hexadecimalInteger); expectType(nonInteger); expectType(infinityInteger); +expectType(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; expectType<1.5>(float); -expectType<1.5>(floatMixed); +expectType<1.5 | -1.5>(floatMixed); expectType(nonFloat); -expectType(infinityFloat); // According to Number.isInteger +expectType(infinityFloat); // Negative declare const negative: Negative<-1 | -1n | 0 | 0n | 1 | 1n>;