From 94bb1c00cc6224573a20b539128ba85277b8f8c2 Mon Sep 17 00:00:00 2001 From: unional Date: Mon, 3 Jul 2023 14:53:19 -0700 Subject: [PATCH] feat: ArrayPlus.Find, TuplePlus.Find --- .changeset/swift-terms-join.md | 6 ++ type-plus/ts/array/array.find.spec.ts | 53 ----------------- type-plus/ts/array/array.find.ts | 34 ----------- type-plus/ts/array/array_plus.find.spec.ts | 40 +++++++++++++ type-plus/ts/array/array_plus.find.ts | 35 ++++++++++++ type-plus/ts/array/array_plus.ts | 6 +- type-plus/ts/array/find_first.spec.ts | 66 ++++++++++++++++++++++ type-plus/ts/array/find_first.ts | 36 ++++++++++++ type-plus/ts/array/readme.md | 13 +++++ type-plus/ts/index.ts | 6 +- type-plus/ts/tuple/tuple_plus.find.spec.ts | 45 +++++++++++++++ type-plus/ts/tuple/tuple_plus.find.ts | 33 +++++++++++ type-plus/ts/tuple/tuple_plus.pad_start.ts | 53 +++++++++-------- type-plus/ts/tuple/tuple_plus.ts | 1 + 14 files changed, 307 insertions(+), 120 deletions(-) create mode 100644 .changeset/swift-terms-join.md delete mode 100644 type-plus/ts/array/array.find.spec.ts delete mode 100644 type-plus/ts/array/array.find.ts create mode 100644 type-plus/ts/array/array_plus.find.spec.ts create mode 100644 type-plus/ts/array/array_plus.find.ts create mode 100644 type-plus/ts/array/find_first.spec.ts create mode 100644 type-plus/ts/array/find_first.ts create mode 100644 type-plus/ts/tuple/tuple_plus.find.spec.ts create mode 100644 type-plus/ts/tuple/tuple_plus.find.ts diff --git a/.changeset/swift-terms-join.md b/.changeset/swift-terms-join.md new file mode 100644 index 0000000000..3e2468f3b7 --- /dev/null +++ b/.changeset/swift-terms-join.md @@ -0,0 +1,6 @@ +--- +"type-plus": minor +--- + +Improve `FindFirst`, +add `ArrayPlus.Find` and `TuplePlus.Find` diff --git a/type-plus/ts/array/array.find.spec.ts b/type-plus/ts/array/array.find.spec.ts deleted file mode 100644 index 17adc0e0cb..0000000000 --- a/type-plus/ts/array/array.find.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { test, it } from '@jest/globals' -import { testType, type FindFirst, ArrayPlus } from '../index.js' - -test('behavior of array.find()', () => { - const array = [1, 2, '3'] - const r = array.find(x => typeof x === 'number') - testType.equal(true) -}) - -test('behavior of tuple.find()', () => { - const tuple = [1, 2, '3'] as const - const r = tuple.find(x => typeof x === 'number') - testType.equal(true) -}) - -it('returns T | undefined for T[] if T satisfies Criteria', () => { - testType.equal, never>(true) - testType.equal, number | undefined>(true) - testType.equal, number>, 1 | 2 | undefined>(true) -}) - -it('returns never for empty tuple', () => { - testType.equal, never>(true) -}) - -it('pick first type matching criteria', () => { - testType.equal, 1>(true) - testType.equal, 'x'>(true) - testType.equal, true>(true) -}) - -it('uses widen type to match literal types', () => { - testType.equal, 1>(true) - testType.equal, 'x'>(true) - testType.equal, true>(true) -}) - -it('no match gets never', () => { - type Actual = FindFirst<[true, 1, 'x'], 2> - testType.equal(true) -}) - -it('pick object', () => { - type Actual = FindFirst< - [{ name: 'a', type: 1 }, { name: 'b', type: 2 }, { name: 'c', type: 3 }, { name: 'b', type: 4 }], - { name: 'b' } - >['type'] - testType.equal(true) -}) - -it('is available as ArrayPlus.Find', () => { - testType.equal, 1>(true) -}) diff --git a/type-plus/ts/array/array.find.ts b/type-plus/ts/array/array.find.ts deleted file mode 100644 index f823aaba01..0000000000 --- a/type-plus/ts/array/array.find.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { TupleType } from '../tuple/tuple_type.js' - -/** - * 🦴 *utilities* - * - * Gets the first type in the array or tuple that matches the `Criteria`. - * - * If the `Criteria` is not met, it will return `never'. - * - * For `Array`, it will return `T | undefined` if `T` satisfies `Criteria`. - * - * @example - * ```ts - * ArrayPlus.Find, number> // 1 | 2 | undefined - * - * ArrayPlus.Find<[true, 1, 'x', 3], string> // 'x' - * ``` - */ -export type FindFirst = TupleType< - A, - A['length'] extends 0 - ? never - : A extends [infer Head, ...infer Tail] - ? Head extends Criteria - ? Head - : FindFirst - : never, - A extends Array ? (T extends Criteria ? T | undefined : never) : never -> - -/** - * @deprecated use FindFirst - */ -export type First = FindFirst diff --git a/type-plus/ts/array/array_plus.find.spec.ts b/type-plus/ts/array/array_plus.find.spec.ts new file mode 100644 index 0000000000..065a27fe73 --- /dev/null +++ b/type-plus/ts/array/array_plus.find.spec.ts @@ -0,0 +1,40 @@ +import { it, test } from '@jest/globals' +import { testType, type ArrayPlus } from '../index.js' + +test('behavior of array.find()', () => { + const array = [1, 2, '3'] + const r = array.find(x => typeof x === 'number') + testType.equal(true) +}) + +it('returns never if the type in the array does not satisfy the criteria', () => { + testType.equal, never>(true) +}) + +it('returns T if T satisfies the Criteria', () => { + testType.equal, number>(true) +}) + +it('returns Criteria | undefined if T is a widen type of Criteria', () => { + testType.equal, 1 | undefined>(true) + testType.equal, 1>, 1 | undefined>(true) + testType.equal, { a: 1 }>, { a: 1 } | undefined>(true) +}) + +it('does not support tuple', () => { + testType.equal< + ArrayPlus.Find<[true, 1, 'x', 3], number>, + 'does not support tuple. Please use `FindFirst` or `TuplePlus.Find` instead.'>(true) +}) + +it('can override widen case', () => { + testType.equal, never>(true) +}) + +it('returns T | undefined for T[] if T is a union satisfies the Criteria', () => { + // adding `undefined` to the result better match the behavior in JavaScript, + // as an array of `Array` can contains only `string` or `number`. + // so `Find, string>` returns `string | undefined`. + testType.equal, number>, number | undefined>(true) + testType.equal, number>, 1 | 2 | undefined>(true) +}) diff --git a/type-plus/ts/array/array_plus.find.ts b/type-plus/ts/array/array_plus.find.ts new file mode 100644 index 0000000000..07377ab195 --- /dev/null +++ b/type-plus/ts/array/array_plus.find.ts @@ -0,0 +1,35 @@ +import type { IsTuple } from '../tuple/tuple_type.js' +import type { IsUnion } from '../union/union.js' + +/** + * 🦴 *utilities* + * + * Gets the first type in the array that matches the `Criteria`. + * + * + * For `Array`, it will return `T | undefined` if `T` satisfies `Criteria`. + * + * @example + * ```ts + * ArrayPlus.Find, number> // 1 | 2 | undefined + * + * ArrayPlus.Find<[true, 1, 'x', 3], string> // 'x' + * ``` + */ +export type Find = + IsTuple< + A, + Cases['tuple'], + A extends Array + ? (T extends Criteria + ? T + : Criteria extends T ? Cases['widen'] : never) extends infer R + ? IsUnion : never + : never + > diff --git a/type-plus/ts/array/array_plus.ts b/type-plus/ts/array/array_plus.ts index 166a022344..41708c91af 100644 --- a/type-plus/ts/array/array_plus.ts +++ b/type-plus/ts/array/array_plus.ts @@ -1,11 +1,11 @@ export type { At } from './array.at.js' -export type { Concat } from './array_plus.concat.js' export type { Entries } from './array.entries.js' -export type { FindFirst as Find } from './array.find.js' export type { FindLast } from './array.find_last.js' export type { Reverse } from './array.reverse.js' export type { Some } from './array.some.js' export type { IndexAt, IsIndexOutOfBound } from './array_index.js' +export type { Concat } from './array_plus.concat.js' export type { Filter } from './array_plus.filter.js' -export type { SplitAt } from './array_plus.split_at.js' +export type { Find } from './array_plus.find.js' export type { PadStart } from './array_plus.pad_start.js' +export type { SplitAt } from './array_plus.split_at.js' diff --git a/type-plus/ts/array/find_first.spec.ts b/type-plus/ts/array/find_first.spec.ts new file mode 100644 index 0000000000..b716d3f1ea --- /dev/null +++ b/type-plus/ts/array/find_first.spec.ts @@ -0,0 +1,66 @@ +import { it } from '@jest/globals' +import { testType, type FindFirst } from '../index.js' +import { describe } from 'node:test' + +describe('For Array', () => { + it('returns never if the type in the array does not satisfy the criteria', () => { + testType.equal, never>(true) + }) + + it('returns T if T satisfies the Criteria', () => { + testType.equal, number>(true) + }) + + it('returns Criteria | undefined if T is a widen type of Criteria', () => { + testType.equal, 1 | undefined>(true) + testType.equal, 1>, 1 | undefined>(true) + testType.equal, { a: 1 }>, { a: 1 } | undefined>(true) + }) + + it('can override widen case', () => { + testType.equal, never>(true) + }) + + it('returns T | undefined for T[] if T is a union satisfies the Criteria', () => { + // adding `undefined` to the result better match the behavior in JavaScript, + // as an array of `Array` can contains only `string` or `number`. + // so `Find, string>` returns `string | undefined`. + testType.equal, number>, number | undefined>(true) + testType.equal, number>, 1 | 2 | undefined>(true) + }) +}) + +describe('for Tuple', () => { + it('returns never for empty tuple', () => { + testType.equal, never>(true) + }) + + it('can override empty tuple case', () => { + testType.equal, 1>(true) + }) + + it('pick first type matching criteria', () => { + testType.equal, 1>(true) + testType.equal, 'x'>(true) + testType.equal, true>(true) + }) + + it('uses widen type to match literal types', () => { + testType.equal, 1>(true) + testType.equal, 'x'>(true) + testType.equal, true>(true) + }) + + it('no match gets never', () => { + type Actual = FindFirst<[true, 1, 'x'], 2> + testType.equal(true) + }) + + it('pick object', () => { + type Actual = FindFirst< + [{ name: 'a', type: 1 }, { name: 'b', type: 2 }, { name: 'c', type: 3 }, { name: 'b', type: 4 }], + { name: 'b' } + >['type'] + testType.equal(true) + }) +}) diff --git a/type-plus/ts/array/find_first.ts b/type-plus/ts/array/find_first.ts new file mode 100644 index 0000000000..75052fb4a0 --- /dev/null +++ b/type-plus/ts/array/find_first.ts @@ -0,0 +1,36 @@ +import type { TupleType } from '../tuple/tuple_type.js' +import type { Find as TupleFind } from '../tuple/tuple_plus.find.js' +import type { Find as ArrayFind } from './array_plus.find.js' + +/** + * 🦴 *utilities* + * + * Gets the first type in the array or tuple that matches the `Criteria`. + * + * If the `Criteria` is not met, it will return `never'. + * + * For `Array`, it will return `T | undefined` if `T` satisfies `Criteria`. + * + * @example + * ```ts + * FindFirst, number> // 1 | 2 | undefined + * + * FindFirst<[true, 1, 'x', 3], string> // 'x' + * ``` + */ +export type FindFirst = TupleType< + A, + TupleFind, + ArrayFind +> + +/** + * @deprecated use FindFirst + */ +export type First = FindFirst diff --git a/type-plus/ts/array/readme.md b/type-plus/ts/array/readme.md index aa8258e1bd..7291bc377e 100644 --- a/type-plus/ts/array/readme.md +++ b/type-plus/ts/array/readme.md @@ -129,6 +129,19 @@ You are encouraged to use `[...A, ...B]` directly. ## [`FindFirst`](./array.find.ts) +`FindFirst` + +🦴 *utilities* + +Gets the first type in the array or tuple that matches the `Criteria`. + +```ts +import type { FindFirst } from 'type-plus' + +FindFirst, number> // 1 | 2 | undefined +FindFirst<[true, 1, 'x', 3], string> // 'x' +``` + ## [`FineLast`](./array.find_last.ts) ## [`Some`](./array.some.ts) diff --git a/type-plus/ts/index.ts b/type-plus/ts/index.ts index e837984eb1..d7fce679a1 100644 --- a/type-plus/ts/index.ts +++ b/type-plus/ts/index.ts @@ -1,6 +1,6 @@ export type { AnyType, IsAny, IsNotAny, NotAnyType } from './any/any_type.js' export type { At } from './array/array.at.js' -export type { FindFirst } from './array/array.find.js' +export type { FindFirst } from './array/find_first.js' export type { FindLast } from './array/array.find_last.js' export type { Some } from './array/array.some.js' export type { Concat } from './array/array_plus.concat.js' @@ -116,7 +116,3 @@ export type { IsNotUnknown, IsUnknown, NotUnknownType, UnknownType } from './unk export * from './unpartial.js' export * from './utils/index.js' export type { IsNotVoid, IsVoid, NotVoidType, VoidType } from './void/void_type.js' - - - - diff --git a/type-plus/ts/tuple/tuple_plus.find.spec.ts b/type-plus/ts/tuple/tuple_plus.find.spec.ts new file mode 100644 index 0000000000..5049d03c88 --- /dev/null +++ b/type-plus/ts/tuple/tuple_plus.find.spec.ts @@ -0,0 +1,45 @@ +import { it, test } from '@jest/globals' +import { testType, type TuplePlus } from '../index.js' + +test('behavior of tuple.find()', () => { + const tuple: [1, 2, '3'] = [1, 2, '3'] + const r = tuple.find(x => typeof x === 'number') + testType.equal(true) +}) + +it('returns never for empty tuple', () => { + testType.equal, never>(true) +}) + +it('can override empty tuple case', () => { + testType.equal, 1>(true) +}) + +it('does not work with array type', () => { + testType.equal, 'does not support array. Please use `FindFirst` or `ArrayPlus.Find` instead.'>(true) +}) + +it('pick first type matching criteria', () => { + testType.equal, 1>(true) + testType.equal, 'x'>(true) + testType.equal, true>(true) +}) + +it('uses widen type to match literal types', () => { + testType.equal, 1>(true) + testType.equal, 'x'>(true) + testType.equal, true>(true) +}) + +it('no match gets never', () => { + type Actual = TuplePlus.Find<[true, 1, 'x'], 2> + testType.equal(true) +}) + +it('pick object', () => { + type Actual = TuplePlus.Find< + [{ name: 'a', type: 1 }, { name: 'b', type: 2 }, { name: 'c', type: 3 }, { name: 'b', type: 4 }], + { name: 'b' } + >['type'] + testType.equal(true) +}) diff --git a/type-plus/ts/tuple/tuple_plus.find.ts b/type-plus/ts/tuple/tuple_plus.find.ts new file mode 100644 index 0000000000..cfba0ce8c2 --- /dev/null +++ b/type-plus/ts/tuple/tuple_plus.find.ts @@ -0,0 +1,33 @@ + +/** + * 🦴 *utilities* + * + * Gets the first type in the array or tuple that matches the `Criteria`. + * + * If the `Criteria` is not met, it will return `never'. + * + * For `Array`, it will return `T | undefined` if `T` satisfies `Criteria`. + * + * @example + * ```ts + * ArrayPlus.Find, number> // 1 | 2 | undefined + * + * ArrayPlus.Find<[true, 1, 'x', 3], string> // 'x' + * ``` + */ +export type Find = + number extends A['length'] + ? Cases['array'] + : (A['length'] extends 0 + ? Cases['empty_tuple'] + : (A extends [infer Head, ...infer Tail] + ? (Head extends Criteria + ? Head + : Find) + : never)) diff --git a/type-plus/ts/tuple/tuple_plus.pad_start.ts b/type-plus/ts/tuple/tuple_plus.pad_start.ts index a9a7f0c5e8..6ea8a38d0e 100644 --- a/type-plus/ts/tuple/tuple_plus.pad_start.ts +++ b/type-plus/ts/tuple/tuple_plus.pad_start.ts @@ -17,39 +17,42 @@ * PadStart<[1, 2, 3], 5> // [unknown, unknown, 1, 2, 3] * ``` */ -export type PadStart = PadStartDevice< +export type PadStart = PadStart.Device< Tuple, MaxLength, PadWith, [] > -type PadStartDevice< - Source extends unknown[], - MaxLength extends number, - PadWith, - Result extends unknown[] -> = - Result['length'] extends MaxLength - ? ( - Source extends [] - ? Result - : ( - Source extends [...infer Head, infer Tail] - ? ( - [Tail, ...Result] extends infer R extends unknown[] - ? PadStartDevice +export namespace PadStart { + export type Device< + Source extends unknown[], + MaxLength extends number, + PadWith, + Result extends unknown[] + > = + Result['length'] extends MaxLength + ? ( + Source extends [] + ? Result + : ( + Source extends [...infer Head, infer Tail] + ? ( + [Tail, ...Result] extends infer R extends unknown[] + ? Device + : never + ) : never ) - : never ) - ) - : ( - Source extends [] - ? PadStartDevice : ( - Source extends [...infer Head, infer Tail] - ? PadStartDevice - : Source + Source extends [] + ? Device + : ( + Source extends [...infer Head, infer Tail] + ? Device + : Source + ) ) - ) + +} diff --git a/type-plus/ts/tuple/tuple_plus.ts b/type-plus/ts/tuple/tuple_plus.ts index 51534d7d77..527b440803 100644 --- a/type-plus/ts/tuple/tuple_plus.ts +++ b/type-plus/ts/tuple/tuple_plus.ts @@ -1,2 +1,3 @@ export type { Filter } from './tuple_plus.filter.js' +export type { Find } from './tuple_plus.find.js' export type { PadStart } from './tuple_plus.pad_start.js'