Skip to content

Commit

Permalink
fix: notMatch and unionNotMatch
Browse files Browse the repository at this point in the history
  • Loading branch information
unional committed Jul 5, 2023
1 parent ae99a68 commit 91211c9
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 61 deletions.
9 changes: 9 additions & 0 deletions .changeset/odd-kings-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"type-plus": patch
---

Rename `caseNoMatch` to `caseNotMatch`.
Rename `caseUnionMiss` to `caseUnionNotMatch`.

Change `caseUnionNotMatch` default from `undefined` to `never`,
making it defaults to the type behavior instead of JavaScript behavior.
31 changes: 17 additions & 14 deletions type-plus/ts/array/array_plus.element_match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,27 @@ import type { MergeOptions } from '../utils/options.js'

/**
* 🦴 *utilities*
* ㊙️ *internal*
* 🔢 *customizable*
*
* Match element in an array or tuple.
* Filter the element `T` in an array or tuple to match `Criteria`.
*
* @typeParam Options['widen'] Allow using narrow type to match widen type.
* e.g. `number, 1` -> `1 | undefined`.
* Default to `true`.
*
* @typeParam Options['caseNoMatch'] Return value when `T` does not match `Criteria`.
* @typeParam Options['caseNotMatch'] Return value when `T` does not match `Criteria`.
* Default to `never`.
*
* @typeParam Options['caseWiden'] Return value when `widen` is true.
* Default to `Criteria | undefined`.
*
* @typeParam Options['caseUnionMiss'] Return value when a branch of the union `T` does not match `Criteria`.
* Default to `undefined`.
* Since it is a union, the result will be join to the matched branch as union.
* @typeParam Options['caseUnionNotMatch'] Return value when a branch of the union `T` does not match `Criteria`.
* Default to `never`.
*
* If you want the type to behave more like JavaScript,
* you can override it to return `undefined`.
*
* Since it is a union, the result will be joined to the matched branch as union.
* e.g. `ElementMatch<1 | 2, 1>` -> `1 | undefined`
*/
export type ElementMatch<
Expand All @@ -36,23 +39,23 @@ export type ElementMatch<
: (C['widen'] extends true
? (Criteria extends T
? C['caseWiden']
: C['caseNoMatch'])
: C['caseNoMatch'])) extends infer R
? IsUnion<T, IsNever<R, R, R | C['caseUnionMiss']>, R>
: C['caseNoMatch'])
: C['caseNotMatch'])
: C['caseNotMatch'])) extends infer R
? IsUnion<T, IsNever<R, R, R | C['caseUnionNotMatch']>, R>
: C['caseNotMatch'])
: never)

export namespace ElementMatch {
export interface Options {
widen?: boolean | undefined,
caseNoMatch?: unknown,
caseNotMatch?: unknown,
caseWiden?: unknown,
caseUnionMiss?: unknown
caseUnionNotMatch?: unknown
}
export interface DefaultOptions<Criteria> {
widen: true,
caseNoMatch: never,
caseNotMatch: never,
caseWiden: Criteria | undefined,
caseUnionMiss: undefined
caseUnionNotMatch: never
}
}
27 changes: 16 additions & 11 deletions type-plus/ts/array/array_plus.find.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ it('returns never if the type in the array does not satisfy the criteria', () =>
})

it('can override no_match case', () => {
testType.equal<ArrayPlus.Find<number[], string, { caseNoMatch: 'a' }>, 'a'>(true)
testType.equal<ArrayPlus.Find<number[], string, { caseNotMatch: 'a' }>, 'a'>(true)
})

it('returns T if T satisfies the Criteria', () => {
Expand Down Expand Up @@ -48,29 +48,34 @@ it('returns never if the union type does not satisfy the Criteria', () => {
testType.equal<ArrayPlus.Find<Array<string | number>, boolean>, never>(true)
})

it('returns T | undefined for T[] if T is a union satisfies the Criteria', () => {
it('returns Criteria if T is a union partially satisfies the Criteria', () => {
testType.equal<ArrayPlus.Find<Array<string | number>, number>, number>(true)
testType.equal<ArrayPlus.Find<Array<1 | 2 | 'x'>, number>, 1 | 2>(true)
})

it('can override unionNotMach to `undefined`', () => {
// 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)
testType.equal<ArrayPlus.Find<Array<string | number>, number, { caseUnionNotMatch: undefined }>, number | undefined>(true)
testType.equal<ArrayPlus.Find<Array<1 | 2 | 'x'>, number, { caseUnionNotMatch: undefined }>, 1 | 2 | undefined>(true)
})

it('handles union_miss and widen cases separately', () => {
it('handles union not match and widen cases separately', () => {
testType.equal<ArrayPlus.Find<Array<string | number>, 1, {
caseWiden: 234,
caseUnionMiss: 123
caseUnionNotMatch: 123
}>, 123 | 234>(true)
})

it('can override the union_miss case', () => {
testType.equal<ArrayPlus.Find<Array<string | number>, number, { caseUnionMiss: never }>, number>(true)
testType.equal<ArrayPlus.Find<Array<string | number>, number, { caseUnionNotMatch: never }>, number>(true)
})

it('will not affect other cases', () => {
testType.equal<ArrayPlus.Find<Array<string | number>, number, { caseNever: 123 }>, number | ArrayPlus.Find.DefaultOptions<unknown>['caseUnionMiss']>(true)
testType.equal<ArrayPlus.Find<never, 1, { caseNoMatch: 123 }>, ArrayPlus.Find.DefaultOptions<unknown>['caseNever']>(true)
testType.equal<ArrayPlus.Find<number[], string, { caseTuple: 123 }>, ArrayPlus.Find.DefaultOptions<unknown>['caseNoMatch']>(true)
testType.equal<ArrayPlus.Find<Array<string | number>, number, { caseNever: 123 }>, number | ArrayPlus.Find.DefaultOptions<unknown>['caseUnionNotMatch']>(true)
testType.equal<ArrayPlus.Find<never, 1, { caseNotMatch: 123 }>, ArrayPlus.Find.DefaultOptions<unknown>['caseNever']>(true)
testType.equal<ArrayPlus.Find<number[], string, { caseTuple: 123 }>, ArrayPlus.Find.DefaultOptions<unknown>['caseNotMatch']>(true)
testType.equal<ArrayPlus.Find<[], 1, { caseWiden: 123 }>, ArrayPlus.Find.DefaultOptions<unknown>['caseTuple']>(true)
testType.equal<ArrayPlus.Find<number[], 1, { caseUnionMiss: 123 }>, ArrayPlus.Find.DefaultOptions<1>['caseWiden']>(true)
testType.equal<ArrayPlus.Find<number[], 1, { caseUnionNotMatch: 123 }>, ArrayPlus.Find.DefaultOptions<1>['caseWiden']>(true)
})
12 changes: 8 additions & 4 deletions type-plus/ts/array/array_plus.find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import type { ElementMatch } from './array_plus.element_match.js'
*
* @typeParam Options['caseNever'] return type when `A` is `never`. Default to `never`.
*
* @typeParam Options['caseNoMatch'] Return value when `T` does not match `Criteria`.
* @typeParam Options['caseNotMatch'] Return value when `T` does not match `Criteria`.
* Default to `never`.
*
* @typeParam Options['caseTuple'] return type when `A` is a tuple. Default to `not supported` message.
Expand All @@ -38,9 +38,13 @@ import type { ElementMatch } from './array_plus.element_match.js'
* Default to `Criteria | undefined`.
* Set it to `never` for a more type-centric behavior
*
* @typeParam Options['caseUnionMiss'] Return value when a branch of the union `T` does not match `Criteria`.
* Default to `undefined`.
* Since it is a union, the result will be join to the matched branch as union.
* @typeParam Options['caseUnionNotMatch'] Return value when a branch of the union `T` does not match `Criteria`.
* Default to `never`.
*
* If you want the type to behave more like JavaScript,
* you can override it to return `undefined`.
*
* Since it is a union, the result will be joined to the matched branch as union.
*/
export type Find<
A extends unknown[],
Expand Down
49 changes: 37 additions & 12 deletions type-plus/ts/array/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,23 +142,23 @@ import type { FindFirst } from 'type-plus'
type R = FindFirst<[true, 1, 'x', 3], string> // 'x'
type R = FindFirst<[true, 1, 'x', 3], number> // 1
type R = FindFirst<[string, number, 1], 1> // widen: 1 | undefined
type R = FindFirst<[true, number | string], string> // unionMiss: string | undefined
type R = FindFirst<[true, number | string], string> // unionNotMatch: string
type R = FindFirst<Array<string>, string> // string
type R = FindFirst<Array<1 | 2 | 'x'>, number> // 1 | 2 | undefined
type R = FindFirst<Array<string | number>, number | string> // string | number
type R = FindFirst<Array<number>, 1> // widen: 1 | undefined
type R = FindFirst<Array<string | number>, number> // unionMiss: number | undefined
type R = FindFirst<Array<string | number>, number> // unionNotMatch: number

type R = FindFirst<[true, 1, 'x'], 2> // never
type R = FindFirst<string[], number> // never

// customization
type R = FindFirst<[number], 1, { widen: false }> // never
type R = FindFirst<[number], 1, { caseWiden: never }> // never
type R = FindFirst<[], 1, { caseEmptyTuple: 2 }> // 2
type R = FindFirst<never, 1, { caseNever: 2 }> // 2
type R = FindFirst<[string], number, { caseNoMatch: 2 }> // 2
type R = FindFirst<[number], 1, { caseWiden: never }> // never
type R = FindFirst<[string | number], number, { caseUnionMiss: never }> // number
type R = FindFirst<[string], number, { caseNotMatch: 2 }> // 2
type R = FindFirst<[string | number], number, { caseUnionNotMatch: undefined }> // number | undefined
```
## [`FindLast`](./array.find_last.ts)
Expand Down Expand Up @@ -292,12 +292,37 @@ type R = ArrayPlus.CommonPropKeys<Array<{ a: 1, b: 1 } | { a: 1, c: 1 }>> // 'a'
type R = ArrayPlus.CommonPropKeys<never, { caseNever: 1 }> // 1
```
### [`ArrayPlus.Concat`](./array.concat.ts#L12)
### [`ArrayPlus.Concat`](./array.concat.ts#l12)
`ArrayPlus.Concat<A, B>`
Alias of [Concat](#concat).
### [`ArrayPlus.ElementMatch`](./array_plus.element_match.ts#l30)
`ArrayPlus.ElementMatch<T, Criteria, Options = { widen, caseNotMatch, caseWiden, caseUnionNotMatch }>`
🌪️ *filter*
🔢 *customizable*
Filter the element `T` in an array or tuple to match `Criteria`.
```ts
import type { ArrayPlus } from 'type-plus'

type R = ArrayPlus.ElementMatch<number, number> // number
type R = ArrayPlus.ElementMatch<1, number> // 1
type R = ArrayPlus.ElementMatch<number, string> // notMatch: never
type R = ArrayPlus.ElementMatch<number, 1> // widen: 1
type R = ArrayPlus.ElementMatch<number | string, number> // unionNotMatch: number

// customization
type R = ArrayPlus.ElementMatch<number, string, { caseNotMatch: 1 }> // 1
type R = ArrayPlus.ElementMatch<number, 1, { widen: false }> // never
type R = ArrayPlus.ElementMatch<number, 1, { caseWiden: never }> // never
type R = ArrayPlus.ElementMatch<number | string, number, { caseUnionNotMatch: undefined }> // number | undefined
```
### [`ArrayPlus.Entries`](./array.entries.ts#L14)
> `ArrayPlus.Entries<A>`
Expand All @@ -312,9 +337,9 @@ type R = ArrayPlus.Entries<Array<string | number>> // Array<[number, string | nu
type R = ArrayPlus.Entries<[1, 2, 3]> // [[0, 1], [1, 2], [2, 3]]
```
### [`ArrayPlus.Find`](./array_plus.find.ts#l45)
### [`ArrayPlus.Find`](./array_plus.find.ts#l49)
`ArrayPlus.Find<A, Criteria, Options { widen, caseNever, caseNoMatch, caseTuple, caseWiden, caseUnionMiss }>`
`ArrayPlus.Find<A, Criteria, Options { widen, caseNever, caseNotMatch, caseTuple, caseWiden, caseUnionNotMatch }>`
🦴 *utilities*
🔢 *customizable*
Expand All @@ -328,17 +353,17 @@ type R = ArrayPlus.Find<Array<string>, string> // string
type R = ArrayPlus.Find<Array<1 | 2 | 'x'>, number> // 1 | 2 | undefined
type R = ArrayPlus.Find<Array<string | number>, number | string> // string | number
type R = ArrayPlus.Find<number[], 1> // widen: 1 | undefined
type R = ArrayPlus.Find<Array<string | number>, number> // union_miss: number | undefined
type R = ArrayPlus.Find<Array<string | number>, number> // unionNotMatch: number

type R = ArrayPlus.Find<string[], number> // never

// customization
type R = ArrayPlus.Find<number[], 1, { widen: false }> // never
type R = ArrayPlus.Find<number[], 1, { caseWiden: never }> // never
type R = ArrayPlus.Find<never, 1, { caseNever: 2 }> // 2
type R = ArrayPlus.Find<string[], number, { caseNoMatch: 2 }> // 2
type R = ArrayPlus.Find<string[], number, { caseNotMatch: 2 }> // 2
type R = ArrayPlus.Find<[], 1, { caseTuple: 2 }> // 2
type R = ArrayPlus.Find<number[], 1, { caseWiden: never }> // never
type R = ArrayPlus.Find<Array<string | number>, number, { caseUnionMiss: never }> // number
type R = ArrayPlus.Find<Array<string | number>, number, { caseUnionNotMatch: undefined }> // number | undefined
```
### [`ArrayPlus.FindLast`](./array.find_last.ts#L17)
Expand Down
5 changes: 4 additions & 1 deletion type-plus/ts/assertion/assert_type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { isConstructor, type AnyConstructor } from '../class/index.js'
import { type AnyFunction } from '../function/any_function.js'

/**
* assert the subject satisfies the specified type T
* 💥 *immediate*
* 🚦 *assertion*
*
* Assert the subject satisfies the specified type T
* @type T the type to check against.
*/
export function assertType<T>(subject: T): asserts subject is T
Expand Down
5 changes: 3 additions & 2 deletions type-plus/ts/assertion/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ They throw an error if the condition is not met, and return nothing otherwise.
These assertion functions are typically used in runtime,
so that that type of the value can be narrowed down.

## [assertType](./assert_type.ts)
## [assertType](./assert_type.ts#l10)

`assertType<T>(subject)`

💥 `immediate`
💥 *immediate*
🚦 *assertion*

It ensures `subject` satisfies `T`.
It is similar to `const x: T = subject` without introducing an unused variable.
Expand Down
12 changes: 6 additions & 6 deletions type-plus/ts/tuple/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,9 @@ import { TuplePlus } from 'type-plus'
type R = TuplePlus.Filter<[1, 2, '3'], number> // [1, 2]
```
### [`TuplePlus.Find`](./tuple_plus.find.ts#l47)
### [`TuplePlus.Find`](./tuple_plus.find.ts#l51)
`TuplePlus.Find<A, Criteria, Options { widen, caseArray, caseEmptyTuple, caseNever, caseNoMatch, caseWiden, caseUnionMiss }>`
`TuplePlus.Find<A, Criteria, Options { widen, caseArray, caseEmptyTuple, caseNever, caseNotMatch, caseWiden, caseUnionNotMatch }>`
🦴 *utilities*
🔢 *customizable*
Expand All @@ -240,18 +240,18 @@ import type { TuplePlus } from 'type-plus'
type R = TuplePlus.Find<[true, 1, 'x', 3], string> // 'x'
type R = TuplePlus.Find<[true, 1, 'x', 3], number> // 1
type R = TuplePlus.Find<[string, number, 1], 1> // widen: 1 | undefined
type R = TuplePlus.Find<[true, number | string], string> // unionMiss: string | undefined
type R = TuplePlus.Find<[true, number | string], string> // unionNotMatch: string

type R = TuplePlus.Find<[true, 1, 'x'], 2> // never

// customization
type R = TuplePlus.Find<[number], 1, { widen: false }> // never
type R = TuplePlus.Find<[number], 1, { caseWiden: never }> // never
type R = TuplePlus.Find<string[], 1, { caseArray: 2 }> // 2
type R = TuplePlus.Find<[], 1, { caseEmptyTuple: 2 }> // 2
type R = TuplePlus.Find<never, 1, { caseNever: 2 }> // 2
type R = TuplePlus.Find<[string], number, { caseNoMatch: 2 }> // 2
type R = TuplePlus.Find<[number], 1, { caseWiden: never }> // never
type R = TuplePlus.Find<[string | number], number, { caseUnionMiss: never }> // number
type R = TuplePlus.Find<[string], number, { caseNotMatch: 2 }> // 2
type R = TuplePlus.Find<[string | number], number, { caseUnionNotMatch: undefined }> // number | undefined
```
### [TuplePlus.PadStart](./tuple_plus.pad_start.ts)
Expand Down
13 changes: 8 additions & 5 deletions type-plus/ts/tuple/tuple_plus.find.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ it('no match gets never', () => {
})

it('can override no_match case', () => {
testType.equal<TuplePlus.Find<[true, 1, 'x'], 2, { caseNoMatch: 1 }>, 1>(true)
testType.equal<TuplePlus.Find<[true, 1, 'x'], 2, { caseNotMatch: 1 }>, 1>(true)
})

it('pick first type matching criteria', () => {
Expand Down Expand Up @@ -66,12 +66,15 @@ it('can disable widen', () => {
testType.equal<TuplePlus.Find<[number], 1, { widen: false }>, never>(true)
})

it('returns T | undefined for element T if T is a union satisfies the Criteria', () => {
testType.equal<TuplePlus.Find<[string | number], number>, number | undefined>(true)
it('returns Criteria if T is a union partially satisfies the Criteria', () => {
testType.equal<TuplePlus.Find<[string | number], number>, number>(true)
})

it('can override the union_miss case', () => {
testType.equal<TuplePlus.Find<[string | number], number, { caseUnionMiss: never }>, number>(true)
it('can return T | undefined by overriding unionNotMach to `undefined`', () => {
// 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<TuplePlus.Find<[string | number], number, { caseUnionNotMatch: undefined }>, number | undefined>(true)
})

it('pick object', () => {
Expand Down
16 changes: 10 additions & 6 deletions type-plus/ts/tuple/tuple_plus.find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,20 @@ import type { TupleType } from './tuple_type.js'
*
* @typeParam Options['caseNever'] return type when `A` is `never`. Default to `never`.
*
* @typeParam Options['caseNoMatch'] Return value when `T` does not match `Criteria`.
* @typeParam Options['caseNotMatch'] Return value when `T` does not match `Criteria`.
* Default to `never`.
*
* @typeParam Options['caseWiden'] return type when `T` in `A` is a widen type of `Criteria`.
* Default to `Criteria | undefined`.
* Set it to `never` for a more type-centric behavior
*
* @typeParam Options['caseUnionMiss'] Return value when a branch of the union `T` does not match `Criteria`.
* Default to `undefined`.
* Since it is a union, the result will be join to the matched branch as union.
* @typeParam Options['caseUnionNotMatch'] Return value when a branch of the union `T` does not match `Criteria`.
* Default to `never`.
*
* If you want the type to behave more like JavaScript,
* you can override it to return `undefined`.
*
* Since it is a union, the result will be joined to the matched branch as union.
*/
export type Find<
A extends unknown[],
Expand All @@ -64,12 +68,12 @@ export namespace Find {
Criteria,
Options extends Find.Options
> = A['length'] extends 0
? Options['caseNoMatch']
? Options['caseNotMatch']
: (A extends [infer Head, ...infer Tail]
? ElementMatch<
Head,
Criteria,
MergeOptions<{ caseNoMatch: Device<Tail, Criteria, Options> }, Options>
MergeOptions<{ caseNotMatch: Device<Tail, Criteria, Options> }, Options>
>
: never)
export interface Options extends ElementMatch.Options, NeverType.Options {
Expand Down

0 comments on commit 91211c9

Please sign in to comment.