Skip to content

Commit

Permalink
add TupleOf and TupleOfAtLeast types, isTupleOf and `isTupleOfA…
Browse files Browse the repository at this point in the history
…tLeast` type guards (#2830)

Co-authored-by: Giulio Canti <[email protected]>
  • Loading branch information
2 people authored and tim-smart committed Jun 6, 2024
1 parent 9305b76 commit b53f69b
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .changeset/purple-shrimps-perform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"effect": minor
---

Types: implement `TupleOf` and `TupleOfAtLeast` types

Predicate: implement `isTupleOf` and `isTupleOfAtLeast` type guards
18 changes: 18 additions & 0 deletions packages/effect/dtslint/Predicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,24 @@ unknowns.filter(Predicate.isRecord)
// $ExpectType { readonly [x: string]: unknown; readonly [x: symbol]: unknown; }[]
unknowns.filter(Predicate.isReadonlyRecord)

// -------------------------------------------------------------------------------------
// isTupleOf
// -------------------------------------------------------------------------------------

if (Predicate.isTupleOf(3)(unknowns)) {
// $ExpectType [unknown, unknown, unknown]
unknowns
}

// -------------------------------------------------------------------------------------
// isTupleOfAtLeast
// -------------------------------------------------------------------------------------

if (Predicate.isTupleOfAtLeast(3)(unknowns)) {
// $ExpectType [unknown, unknown, unknown, ...unknown[]]
unknowns
}

// -------------------------------------------------------------------------------------
// compose
// -------------------------------------------------------------------------------------
Expand Down
14 changes: 14 additions & 0 deletions packages/effect/dtslint/Types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
import { hole } from "effect/Function"
import type * as Types from "effect/Types"

// -------------------------------------------------------------------------------------
// TupleOf
// -------------------------------------------------------------------------------------

// $ExpectType [number, number, number]
hole<Types.TupleOf<3, number>>()

// -------------------------------------------------------------------------------------
// TupleOfAtLeast
// -------------------------------------------------------------------------------------

// $ExpectType [number, number, number, ...number[]]
hole<Types.TupleOfAtLeast<3, number>>()

// -------------------------------------------------------------------------------------
// UnionToIntersection
// -------------------------------------------------------------------------------------
Expand Down
59 changes: 59 additions & 0 deletions packages/effect/src/Predicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import { dual, isFunction as isFunction_ } from "./Function.js"
import type { TypeLambda } from "./HKT.js"
import type { TupleOf, TupleOfAtLeast } from "./Types.js"

/**
* @category models
Expand Down Expand Up @@ -52,6 +53,64 @@ export const mapInput: {
<A, B>(self: Predicate<A>, f: (b: B) => A): Predicate<B>
} = dual(2, <A, B>(self: Predicate<A>, f: (b: B) => A): Predicate<B> => (b) => self(f(b)))

/**
* Determine if an `Array` is a tuple with exactly `N` elements, narrowing down the type to `TupleOf`.
*
* An `Array` is considered to be a `TupleOf` if its length is exactly `N`.
*
* @param self - The `Array` to check.
* @param n - The exact number of elements that the `Array` should have to be considered a `TupleOf`.
*
* @example
* import { isTupleOf } from "effect/Predicate"
*
* assert.deepStrictEqual(isTupleOf([1, 2, 3], 3), true);
* assert.deepStrictEqual(isTupleOf([1, 2, 3], 2), false);
* assert.deepStrictEqual(isTupleOf([1, 2, 3], 4), false);
*
* const arr: number[] = [1, 2, 3];
* if (isTupleOf(arr, 3)) {
* console.log(arr);
* // ^? [number, number, number]
* }
*
* @category guards
* @since 3.3.0
*/
export const isTupleOf: {
<N extends number>(n: N): <T>(self: Array<T>) => self is TupleOf<N, T>
<T, N extends number>(self: Array<T>, n: N): self is TupleOf<N, T>
} = dual(2, <T, N extends number>(self: Array<T>, n: N): self is TupleOf<N, T> => self.length === n)

/**
* Determine if an `Array` is a tuple with at least `N` elements, narrowing down the type to `TupleOfAtLeast`.
*
* An `Array` is considered to be a `TupleOfAtLeast` if its length is at least `N`.
*
* @param self - The `Array` to check.
* @param n - The minimum number of elements that the `Array` should have to be considered a `TupleOfAtLeast`.
*
* @example
* import { isTupleOfAtLeast } from "effect/Predicate"
*
* assert.deepStrictEqual(isTupleOfAtLeast([1, 2, 3], 3), true);
* assert.deepStrictEqual(isTupleOfAtLeast([1, 2, 3], 2), true);
* assert.deepStrictEqual(isTupleOfAtLeast([1, 2, 3], 4), false);
*
* const arr: number[] = [1, 2, 3, 4];
* if (isTupleOfAtLeast(arr, 3)) {
* console.log(arr);
* // ^? [number, number, number, ...number[]]
* }
*
* @category guards
* @since 3.3.0
*/
export const isTupleOfAtLeast: {
<N extends number>(n: N): <T>(self: Array<T>) => self is TupleOfAtLeast<N, T>
<T, N extends number>(self: Array<T>, n: N): self is TupleOfAtLeast<N, T>
} = dual(2, <T, N extends number>(self: Array<T>, n: N): self is TupleOfAtLeast<N, T> => self.length >= n)

/**
* Tests if a value is `truthy`.
*
Expand Down
53 changes: 53 additions & 0 deletions packages/effect/src/Tuple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,56 @@ export const appendElement: {
- swap
*/

export {
/**
* Determine if an `Array` is a tuple with exactly `N` elements, narrowing down the type to `TupleOf`.
*
* An `Array` is considered to be a `TupleOf` if its length is exactly `N`.
*
* @param self - The `Array` to check.
* @param n - The exact number of elements that the `Array` should have to be considered a `TupleOf`.
*
* @example
* import { isTupleOf } from "effect/Tuple"
*
* assert.deepStrictEqual(isTupleOf([1, 2, 3], 3), true);
* assert.deepStrictEqual(isTupleOf([1, 2, 3], 2), false);
* assert.deepStrictEqual(isTupleOf([1, 2, 3], 4), false);
*
* const arr: number[] = [1, 2, 3];
* if (isTupleOf(arr, 3)) {
* console.log(arr);
* // ^? [number, number, number]
* }
*
* @category guards
* @since 3.3.0
*/
isTupleOf,
/**
* Determine if an `Array` is a tuple with at least `N` elements, narrowing down the type to `TupleOfAtLeast`.
*
* An `Array` is considered to be a `TupleOfAtLeast` if its length is at least `N`.
*
* @param self - The `Array` to check.
* @param n - The minimum number of elements that the `Array` should have to be considered a `TupleOfAtLeast`.
*
* @example
* import { isTupleOfAtLeast } from "effect/Tuple"
*
* assert.deepStrictEqual(isTupleOfAtLeast([1, 2, 3], 3), true);
* assert.deepStrictEqual(isTupleOfAtLeast([1, 2, 3], 2), true);
* assert.deepStrictEqual(isTupleOfAtLeast([1, 2, 3], 4), false);
*
* const arr: number[] = [1, 2, 3, 4];
* if (isTupleOfAtLeast(arr, 3)) {
* console.log(arr);
* // ^? [number, number, number, ...number[]]
* }
*
* @category guards
* @since 3.3.0
*/
isTupleOfAtLeast
} from "./Predicate.js"
48 changes: 48 additions & 0 deletions packages/effect/src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,54 @@
* @since 2.0.0
*/

type _TupleOf<T, N extends number, R extends Array<unknown>> = R["length"] extends N ? R : _TupleOf<T, N, [T, ...R]>

/**
* Represents a tuple with a fixed number of elements of type `T`.
*
* This type constructs a tuple that has exactly `N` elements of type `T`.
*
* @typeParam N - The number of elements in the tuple.
* @typeParam T - The type of elements in the tuple.
*
* @example
* import { TupleOf } from "effect/Types"
*
* // A tuple with exactly 3 numbers
* const example1: TupleOf<3, number> = [1, 2, 3]; // valid
* // @ts-expect-error
* const example2: TupleOf<3, number> = [1, 2]; // invalid
* // @ts-expect-error
* const example3: TupleOf<3, number> = [1, 2, 3, 4]; // invalid
*
* @category tuples
* @since 3.3.0
*/
export type TupleOf<N extends number, T> = N extends N ? number extends N ? Array<T> : _TupleOf<T, N, []> : never

/**
* Represents a tuple with at least `N` elements of type `T`.
*
* This type constructs a tuple that has a fixed number of elements `N` of type `T` at the start,
* followed by any number (including zero) of additional elements of the same type `T`.
*
* @typeParam N - The minimum number of elements in the tuple.
* @typeParam T - The type of elements in the tuple.
*
* @example
* import { TupleOfAtLeast } from "effect/Types"
*
* // A tuple with at least 3 numbers
* const example1: TupleOfAtLeast<3, number> = [1, 2, 3]; // valid
* const example2: TupleOfAtLeast<3, number> = [1, 2, 3, 4, 5]; // valid
* // @ts-expect-error
* const example3: TupleOfAtLeast<3, number> = [1, 2]; // invalid
*
* @category tuples
* @since 3.3.0
*/
export type TupleOfAtLeast<N extends number, T> = [...TupleOf<N, T>, ...Array<T>]

/**
* Returns the tags in a type.
* @example
Expand Down
12 changes: 12 additions & 0 deletions packages/effect/test/Predicate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,4 +322,16 @@ describe("Predicate", () => {
assert.deepStrictEqual(_.isReadonlyRecord(null), false)
assert.deepStrictEqual(_.isReadonlyRecord(undefined), false)
})

it("isTupleOf", () => {
assert.deepStrictEqual(_.isTupleOf([1, 2, 3], 3), true)
assert.deepStrictEqual(_.isTupleOf([1, 2, 3], 4), false)
assert.deepStrictEqual(_.isTupleOf([1, 2, 3], 2), false)
})

it("isTupleOfAtLeast", () => {
assert.deepStrictEqual(_.isTupleOfAtLeast([1, 2, 3], 3), true)
assert.deepStrictEqual(_.isTupleOfAtLeast([1, 2, 3], 2), true)
assert.deepStrictEqual(_.isTupleOfAtLeast([1, 2, 3], 4), false)
})
})

0 comments on commit b53f69b

Please sign in to comment.