Skip to content

Commit

Permalink
feat: ArrayPlus.Find, TuplePlus.Find
Browse files Browse the repository at this point in the history
  • Loading branch information
unional committed Jul 3, 2023
1 parent a7739e0 commit 94bb1c0
Show file tree
Hide file tree
Showing 14 changed files with 307 additions and 120 deletions.
6 changes: 6 additions & 0 deletions .changeset/swift-terms-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"type-plus": minor
---

Improve `FindFirst`,
add `ArrayPlus.Find` and `TuplePlus.Find`
53 changes: 0 additions & 53 deletions type-plus/ts/array/array.find.spec.ts

This file was deleted.

34 changes: 0 additions & 34 deletions type-plus/ts/array/array.find.ts

This file was deleted.

40 changes: 40 additions & 0 deletions type-plus/ts/array/array_plus.find.spec.ts
Original file line number Diff line number Diff line change
@@ -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<typeof r, number | string | undefined>(true)
})

it('returns never if the type in the array does not satisfy the criteria', () => {
testType.equal<ArrayPlus.Find<string[], number>, never>(true)
})

it('returns T if T satisfies the Criteria', () => {
testType.equal<ArrayPlus.Find<number[], number>, number>(true)
})

it('returns Criteria | undefined if T is a widen type of Criteria', () => {
testType.equal<ArrayPlus.Find<number[], 1>, 1 | undefined>(true)
testType.equal<ArrayPlus.Find<Array<string | number>, 1>, 1 | undefined>(true)
testType.equal<ArrayPlus.Find<Array<{ a: number } | { b: number }>, { 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<ArrayPlus.Find<number[], 1, { widen: never }>, 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<string | number>` can contains only `string` or `number`.
// so `Find<Array<string | number>, string>` returns `string | undefined`.
testType.equal<ArrayPlus.Find<Array<string | number>, number>, number | undefined>(true)
testType.equal<ArrayPlus.Find<Array<1 | 2 | 'x'>, number>, 1 | 2 | undefined>(true)
})
35 changes: 35 additions & 0 deletions type-plus/ts/array/array_plus.find.ts
Original file line number Diff line number Diff line change
@@ -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<T>`, it will return `T | undefined` if `T` satisfies `Criteria`.
*
* @example
* ```ts
* ArrayPlus.Find<Array<1 | 2 | 'x'>, number> // 1 | 2 | undefined
*
* ArrayPlus.Find<[true, 1, 'x', 3], string> // 'x'
* ```
*/
export type Find<A extends unknown[], Criteria, Cases extends {
tuple?: unknown,
widen?: unknown
} = {
tuple: 'does not support tuple. Please use `FindFirst` or `TuplePlus.Find` instead.',
widen: Criteria | undefined
}> =
IsTuple<
A,
Cases['tuple'],
A extends Array<infer T>
? (T extends Criteria
? T
: Criteria extends T ? Cases['widen'] : never) extends infer R
? IsUnion<T, R | undefined, R> : never
: never
>
6 changes: 3 additions & 3 deletions type-plus/ts/array/array_plus.ts
Original file line number Diff line number Diff line change
@@ -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'
66 changes: 66 additions & 0 deletions type-plus/ts/array/find_first.spec.ts
Original file line number Diff line number Diff line change
@@ -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<FindFirst<string[], number>, never>(true)
})

it('returns T if T satisfies the Criteria', () => {
testType.equal<FindFirst<number[], number>, number>(true)
})

it('returns Criteria | undefined if T is a widen type of Criteria', () => {
testType.equal<FindFirst<number[], 1>, 1 | undefined>(true)
testType.equal<FindFirst<Array<string | number>, 1>, 1 | undefined>(true)
testType.equal<FindFirst<Array<{ a: number } | { b: number }>, { a: 1 }>, { a: 1 } | undefined>(true)
})

it('can override widen case', () => {
testType.equal<FindFirst<number[], 1, { widen: never }>, 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<string | number>` can contains only `string` or `number`.
// so `Find<Array<string | number>, string>` returns `string | undefined`.
testType.equal<FindFirst<Array<string | number>, number>, number | undefined>(true)
testType.equal<FindFirst<Array<1 | 2 | 'x'>, number>, 1 | 2 | undefined>(true)
})
})

describe('for Tuple', () => {
it('returns never for empty tuple', () => {
testType.equal<FindFirst<[], number>, never>(true)
})

it('can override empty tuple case', () => {
testType.equal<FindFirst<[], number, { empty_tuple: 1 }>, 1>(true)
})

it('pick first type matching criteria', () => {
testType.equal<FindFirst<[true, 1, 'x', 3], 1>, 1>(true)
testType.equal<FindFirst<[true, 1, 'x', 3], 'x'>, 'x'>(true)
testType.equal<FindFirst<[true, 1, 'x', 3], true>, true>(true)
})

it('uses widen type to match literal types', () => {
testType.equal<FindFirst<[true, 1, 'x', 3], number>, 1>(true)
testType.equal<FindFirst<[true, 1, 'x', 3], string>, 'x'>(true)
testType.equal<FindFirst<[true, 1, 'x', 3], boolean>, true>(true)
})

it('no match gets never', () => {
type Actual = FindFirst<[true, 1, 'x'], 2>
testType.equal<Actual, never>(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<Actual, 2>(true)
})
})
36 changes: 36 additions & 0 deletions type-plus/ts/array/find_first.ts
Original file line number Diff line number Diff line change
@@ -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<T>`, it will return `T | undefined` if `T` satisfies `Criteria`.
*
* @example
* ```ts
* FindFirst<Array<1 | 2 | 'x'>, number> // 1 | 2 | undefined
*
* FindFirst<[true, 1, 'x', 3], string> // 'x'
* ```
*/
export type FindFirst<A extends unknown[], Criteria, Cases extends {
empty_tuple?: unknown,
widen?: unknown
} = {
empty_tuple: never,
widen: Criteria | undefined
}> = TupleType<
A,
TupleFind<A, Criteria, Cases>,
ArrayFind<A, Criteria, Cases>
>

/**
* @deprecated use FindFirst
*/
export type First<A extends any[], Criteria> = FindFirst<A, Criteria>
13 changes: 13 additions & 0 deletions type-plus/ts/array/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,19 @@ You are encouraged to use `[...A, ...B]` directly.

## [`FindFirst`](./array.find.ts)

`FindFirst<A, Criteria, Cases = { empty_tuple, widen }>`

🦴 *utilities*

Gets the first type in the array or tuple that matches the `Criteria`.

```ts
import type { FindFirst } from 'type-plus'

FindFirst<Array<1 | 2 | 'x'>, number> // 1 | 2 | undefined
FindFirst<[true, 1, 'x', 3], string> // 'x'
```

## [`FineLast`](./array.find_last.ts)

## [`Some`](./array.some.ts)
Expand Down
6 changes: 1 addition & 5 deletions type-plus/ts/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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'




45 changes: 45 additions & 0 deletions type-plus/ts/tuple/tuple_plus.find.spec.ts
Original file line number Diff line number Diff line change
@@ -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<typeof r, 1 | 2 | '3' | undefined>(true)
})

it('returns never for empty tuple', () => {
testType.equal<TuplePlus.Find<[], number>, never>(true)
})

it('can override empty tuple case', () => {
testType.equal<TuplePlus.Find<[], number, { empty_tuple: 1 }>, 1>(true)
})

it('does not work with array type', () => {
testType.equal<TuplePlus.Find<string[], number>, 'does not support array. Please use `FindFirst` or `ArrayPlus.Find` instead.'>(true)
})

it('pick first type matching criteria', () => {
testType.equal<TuplePlus.Find<[true, 1, 'x', 3], 1>, 1>(true)
testType.equal<TuplePlus.Find<[true, 1, 'x', 3], 'x'>, 'x'>(true)
testType.equal<TuplePlus.Find<[true, 1, 'x', 3], true>, true>(true)
})

it('uses widen type to match literal types', () => {
testType.equal<TuplePlus.Find<[true, 1, 'x', 3], number>, 1>(true)
testType.equal<TuplePlus.Find<[true, 1, 'x', 3], string>, 'x'>(true)
testType.equal<TuplePlus.Find<[true, 1, 'x', 3], boolean>, true>(true)
})

it('no match gets never', () => {
type Actual = TuplePlus.Find<[true, 1, 'x'], 2>
testType.equal<Actual, never>(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<Actual, 2>(true)
})
Loading

0 comments on commit 94bb1c0

Please sign in to comment.