From cbac3c2840970b05dd846e7eb538c0f7cf15f712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Rish=C3=B8j?= Date: Wed, 7 Aug 2024 14:30:30 +0200 Subject: [PATCH 1/8] feat(first,last): narrow return types for empty and non-empty arguments --- src/array/first.ts | 2 ++ src/array/last.ts | 2 ++ tests/array/first.test-d.ts | 54 +++++++++++++++++++++++++++++++++++++ tests/array/last.test-d.ts | 54 +++++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 tests/array/first.test-d.ts create mode 100644 tests/array/last.test-d.ts diff --git a/src/array/first.ts b/src/array/first.ts index ca809d7e..40b7e8a1 100644 --- a/src/array/first.ts +++ b/src/array/first.ts @@ -12,8 +12,10 @@ * ``` * @version 12.1.0 */ +export function first(array: readonly [T, ...T[]]): T export function first(array: readonly T[]): T | undefined +export function first(array: readonly [T, ...T[]], defaultValue: U): T export function first(array: readonly T[], defaultValue: U): T | U export function first(array: readonly unknown[], defaultValue?: unknown) { diff --git a/src/array/last.ts b/src/array/last.ts index 71810b9a..c6021406 100644 --- a/src/array/last.ts +++ b/src/array/last.ts @@ -12,8 +12,10 @@ * ``` * @version 12.1.0 */ +export function last(array: readonly [T, ...T[]]): T export function last(array: readonly T[]): T | undefined +export function last(array: readonly [T, ...T[]], defaultValue: U): T export function last(array: readonly T[], defaultValue: U): T | U export function last(array: readonly unknown[], defaultValue?: unknown) { diff --git a/tests/array/first.test-d.ts b/tests/array/first.test-d.ts new file mode 100644 index 00000000..a7f750db --- /dev/null +++ b/tests/array/first.test-d.ts @@ -0,0 +1,54 @@ +import * as _ from 'radashi' +import { expectTypeOf } from 'vitest' + +describe('first types', () => { + test('return type with literal argument', () => { + expectTypeOf(_.first([])).toBeUndefined() + expectTypeOf(_.first([1, 2, 3])).toBeNumber() + }) + test('return type with mutable variable', () => { + const neverList: never[] = [] + const emptyList: number[] = [] + const filledList = [1, 2, 3] + + expectTypeOf(_.first(neverList)).toEqualTypeOf() + expectTypeOf(_.first(emptyList)).toEqualTypeOf() + expectTypeOf(_.first(filledList)).toEqualTypeOf() + }) + test('return type with immutable variable', () => { + const neverList: never[] = [] as const + const emptyList: number[] = [] as const + const filledList = [1, 2, 3] as const + + expectTypeOf(_.first(neverList)).toBeUndefined() + // FIXME: Can this be narrowed to `undefined`? + expectTypeOf(_.first(emptyList)).toEqualTypeOf() + expectTypeOf(_.first(filledList)).toEqualTypeOf<1 | 2 | 3>() + }) +}) + +describe('first types with default value', () => { + test('return type with literal argument', () => { + expectTypeOf(_.first([], false)).toBeBoolean() + expectTypeOf(_.first([1, 2, 3], false)).toBeNumber() + }) + test('return type with mutable variable', () => { + const neverList: never[] = [] + const emptyList: number[] = [] + const filledList = [1, 2, 3] + + expectTypeOf(_.first(neverList, false)).toEqualTypeOf() + expectTypeOf(_.first(emptyList, false)).toEqualTypeOf() + expectTypeOf(_.first(filledList, false)).toEqualTypeOf() + }) + test('return type with immutable variable', () => { + const neverList: never[] = [] as const + const emptyList: number[] = [] as const + const filledList = [1, 2, 3] as const + + expectTypeOf(_.first(neverList, false)).toBeBoolean() + // FIXME: Can this be narrowed to `boolean`? + expectTypeOf(_.first(emptyList, false)).toEqualTypeOf() + expectTypeOf(_.first(filledList, false)).toEqualTypeOf<1 | 2 | 3>() + }) +}) diff --git a/tests/array/last.test-d.ts b/tests/array/last.test-d.ts new file mode 100644 index 00000000..bc07ce78 --- /dev/null +++ b/tests/array/last.test-d.ts @@ -0,0 +1,54 @@ +import * as _ from 'radashi' +import { expectTypeOf } from 'vitest' + +describe('last types', () => { + test('return type with literal argument', () => { + expectTypeOf(_.last([])).toBeUndefined() + expectTypeOf(_.last([1, 2, 3])).toBeNumber() + }) + test('return type with mutable variable', () => { + const neverList: never[] = [] + const emptyList: number[] = [] + const filledList = [1, 2, 3] + + expectTypeOf(_.last(neverList)).toEqualTypeOf() + expectTypeOf(_.last(emptyList)).toEqualTypeOf() + expectTypeOf(_.last(filledList)).toEqualTypeOf() + }) + test('return type with immutable variable', () => { + const neverList: never[] = [] as const + const emptyList: number[] = [] as const + const filledList = [1, 2, 3] as const + + expectTypeOf(_.last(neverList)).toBeUndefined() + // FIXME: Can this be narrowed to `undefined`? + expectTypeOf(_.last(emptyList)).toEqualTypeOf() + expectTypeOf(_.last(filledList)).toEqualTypeOf<1 | 2 | 3>() + }) +}) + +describe('last types with default value', () => { + test('return type with literal argument', () => { + expectTypeOf(_.last([], false)).toBeBoolean() + expectTypeOf(_.last([1, 2, 3], false)).toBeNumber() + }) + test('return type with mutable variable', () => { + const neverList: never[] = [] + const emptyList: number[] = [] + const filledList = [1, 2, 3] + + expectTypeOf(_.last(neverList, false)).toEqualTypeOf() + expectTypeOf(_.last(emptyList, false)).toEqualTypeOf() + expectTypeOf(_.last(filledList, false)).toEqualTypeOf() + }) + test('return type with immutable variable', () => { + const neverList: never[] = [] as const + const emptyList: number[] = [] as const + const filledList = [1, 2, 3] as const + + expectTypeOf(_.last(neverList, false)).toBeBoolean() + // FIXME: Can this be narrowed to `boolean`? + expectTypeOf(_.last(emptyList, false)).toEqualTypeOf() + expectTypeOf(_.last(filledList, false)).toEqualTypeOf<1 | 2 | 3>() + }) +}) From f9ef425a57a48ecb9f14f7e5dff0054feb930179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Rish=C3=B8j?= Date: Thu, 15 Aug 2024 13:42:55 +0200 Subject: [PATCH 2/8] remove superfluous import Co-authored-by: Marlon Passos <1marlonpassos@gmail.com> --- tests/array/first.test-d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/array/first.test-d.ts b/tests/array/first.test-d.ts index a7f750db..737c3414 100644 --- a/tests/array/first.test-d.ts +++ b/tests/array/first.test-d.ts @@ -1,5 +1,4 @@ import * as _ from 'radashi' -import { expectTypeOf } from 'vitest' describe('first types', () => { test('return type with literal argument', () => { From d4216d1694aa43e206c02cb5d3caf1c26dc7c54f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Rish=C3=B8j?= Date: Thu, 15 Aug 2024 13:43:00 +0200 Subject: [PATCH 3/8] remove superfluous import Co-authored-by: Marlon Passos <1marlonpassos@gmail.com> --- tests/array/last.test-d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/array/last.test-d.ts b/tests/array/last.test-d.ts index bc07ce78..44109e7d 100644 --- a/tests/array/last.test-d.ts +++ b/tests/array/last.test-d.ts @@ -1,5 +1,4 @@ import * as _ from 'radashi' -import { expectTypeOf } from 'vitest' describe('last types', () => { test('return type with literal argument', () => { From f029f1b2a68bf659b92365dbb782790d3c183b23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Rish=C3=B8j?= Date: Sat, 9 Nov 2024 22:40:49 +0100 Subject: [PATCH 4/8] fill out `next-minor.md` --- .github/next-minor.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/next-minor.md b/.github/next-minor.md index 2b835552..69e42e9c 100644 --- a/.github/next-minor.md +++ b/.github/next-minor.md @@ -8,4 +8,8 @@ The `####` headline should be short and descriptive of the new functionality. In ## New Features -#### +#### Return type narrowing for `first` and `last` + +Previously, `first` and `last` would return `T | undefined`, making subsequent use of the returned value prone to Typescript warnings. + +With return type narrowing, `first([])` will have return type `undefined`, and `last([1, 2, 3])` will have return type `number`. For mutable arguments, `first` and `last` will still return `T | undefined`, but for immutable arguments (`as const`), return type will be narrowed. From e4a4e1714c1dace407fd3f601149ebf9f7c141b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Rish=C3=B8j?= Date: Sat, 9 Nov 2024 22:58:49 +0100 Subject: [PATCH 5/8] link to PR --- .github/next-minor.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/next-minor.md b/.github/next-minor.md index 69e42e9c..bda10756 100644 --- a/.github/next-minor.md +++ b/.github/next-minor.md @@ -12,4 +12,4 @@ The `####` headline should be short and descriptive of the new functionality. In Previously, `first` and `last` would return `T | undefined`, making subsequent use of the returned value prone to Typescript warnings. -With return type narrowing, `first([])` will have return type `undefined`, and `last([1, 2, 3])` will have return type `number`. For mutable arguments, `first` and `last` will still return `T | undefined`, but for immutable arguments (`as const`), return type will be narrowed. +With return type narrowing in https://github.com/radashi-org/radashi/pull/160, `first([])` will have return type `undefined`, and `last([1, 2, 3])` will have return type `number`. For mutable arguments, `first` and `last` will still return `T | undefined`, but for immutable arguments (`as const` or `readonly`), return type will be narrowed. From 2065a81402ded8d982c2567743231d173344a159 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sun, 10 Nov 2024 09:17:07 -0500 Subject: [PATCH 6/8] Revert "link to PR" This reverts commit e4a4e1714c1dace407fd3f601149ebf9f7c141b9. --- .github/next-minor.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/next-minor.md b/.github/next-minor.md index bda10756..69e42e9c 100644 --- a/.github/next-minor.md +++ b/.github/next-minor.md @@ -12,4 +12,4 @@ The `####` headline should be short and descriptive of the new functionality. In Previously, `first` and `last` would return `T | undefined`, making subsequent use of the returned value prone to Typescript warnings. -With return type narrowing in https://github.com/radashi-org/radashi/pull/160, `first([])` will have return type `undefined`, and `last([1, 2, 3])` will have return type `number`. For mutable arguments, `first` and `last` will still return `T | undefined`, but for immutable arguments (`as const` or `readonly`), return type will be narrowed. +With return type narrowing, `first([])` will have return type `undefined`, and `last([1, 2, 3])` will have return type `number`. For mutable arguments, `first` and `last` will still return `T | undefined`, but for immutable arguments (`as const`), return type will be narrowed. From 93f4c76097458062a601646efa61f3bd222ae8be Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sun, 10 Nov 2024 09:17:09 -0500 Subject: [PATCH 7/8] Revert "fill out `next-minor.md`" This reverts commit f029f1b2a68bf659b92365dbb782790d3c183b23. --- .github/next-minor.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/next-minor.md b/.github/next-minor.md index 69e42e9c..2b835552 100644 --- a/.github/next-minor.md +++ b/.github/next-minor.md @@ -8,8 +8,4 @@ The `####` headline should be short and descriptive of the new functionality. In ## New Features -#### Return type narrowing for `first` and `last` - -Previously, `first` and `last` would return `T | undefined`, making subsequent use of the returned value prone to Typescript warnings. - -With return type narrowing, `first([])` will have return type `undefined`, and `last([1, 2, 3])` will have return type `number`. For mutable arguments, `first` and `last` will still return `T | undefined`, but for immutable arguments (`as const`), return type will be narrowed. +#### From 8371cbb0dbd648917afccf736ed161ab783f3b05 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sun, 10 Nov 2024 12:33:00 -0500 Subject: [PATCH 8/8] improvements --- src/array/first.ts | 16 ++++---- src/array/last.ts | 16 ++++---- tests/array/first.test-d.ts | 75 +++++++++++++++---------------------- tests/array/last.test-d.ts | 75 +++++++++++++++---------------------- 4 files changed, 80 insertions(+), 102 deletions(-) diff --git a/src/array/first.ts b/src/array/first.ts index 40b7e8a1..f70fc71f 100644 --- a/src/array/first.ts +++ b/src/array/first.ts @@ -12,12 +12,14 @@ * ``` * @version 12.1.0 */ -export function first(array: readonly [T, ...T[]]): T -export function first(array: readonly T[]): T | undefined - -export function first(array: readonly [T, ...T[]], defaultValue: U): T -export function first(array: readonly T[], defaultValue: U): T | U - -export function first(array: readonly unknown[], defaultValue?: unknown) { +export function first< + const TArray extends readonly any[], + const TDefault = undefined, +>( + array: TArray, + defaultValue?: TDefault, +): TArray extends readonly [infer TFirst, ...any[]] + ? TFirst + : TArray[number] | TDefault { return array?.length > 0 ? array[0] : defaultValue } diff --git a/src/array/last.ts b/src/array/last.ts index c6021406..7fca7b2a 100644 --- a/src/array/last.ts +++ b/src/array/last.ts @@ -12,12 +12,14 @@ * ``` * @version 12.1.0 */ -export function last(array: readonly [T, ...T[]]): T -export function last(array: readonly T[]): T | undefined - -export function last(array: readonly [T, ...T[]], defaultValue: U): T -export function last(array: readonly T[], defaultValue: U): T | U - -export function last(array: readonly unknown[], defaultValue?: unknown) { +export function last< + const TArray extends readonly any[], + const TDefault = undefined, +>( + array: TArray, + defaultValue?: TDefault, +): TArray extends readonly [...any[], infer TLast] + ? TLast + : TArray[number] | TDefault { return array?.length > 0 ? array[array.length - 1] : defaultValue } diff --git a/tests/array/first.test-d.ts b/tests/array/first.test-d.ts index 737c3414..fd0db352 100644 --- a/tests/array/first.test-d.ts +++ b/tests/array/first.test-d.ts @@ -1,53 +1,40 @@ import * as _ from 'radashi' -describe('first types', () => { - test('return type with literal argument', () => { - expectTypeOf(_.first([])).toBeUndefined() - expectTypeOf(_.first([1, 2, 3])).toBeNumber() +describe('first', () => { + test('inlined array', () => { + expectTypeOf(_.first([])).toEqualTypeOf() + expectTypeOf(_.first([1, 2, 3])).toEqualTypeOf<1>() }) - test('return type with mutable variable', () => { - const neverList: never[] = [] - const emptyList: number[] = [] - const filledList = [1, 2, 3] - - expectTypeOf(_.first(neverList)).toEqualTypeOf() - expectTypeOf(_.first(emptyList)).toEqualTypeOf() - expectTypeOf(_.first(filledList)).toEqualTypeOf() - }) - test('return type with immutable variable', () => { - const neverList: never[] = [] as const - const emptyList: number[] = [] as const - const filledList = [1, 2, 3] as const - - expectTypeOf(_.first(neverList)).toBeUndefined() - // FIXME: Can this be narrowed to `undefined`? - expectTypeOf(_.first(emptyList)).toEqualTypeOf() - expectTypeOf(_.first(filledList)).toEqualTypeOf<1 | 2 | 3>() + + test('variable with empty array', () => { + const emptyArray = [] as never[] + + expectTypeOf(_.first(emptyArray)).toEqualTypeOf() }) -}) -describe('first types with default value', () => { - test('return type with literal argument', () => { - expectTypeOf(_.first([], false)).toBeBoolean() - expectTypeOf(_.first([1, 2, 3], false)).toBeNumber() + test('variable with mutable array', () => { + const array = [1, 2, 3] + + expectTypeOf(_.first(array)).toEqualTypeOf() }) - test('return type with mutable variable', () => { - const neverList: never[] = [] - const emptyList: number[] = [] - const filledList = [1, 2, 3] - - expectTypeOf(_.first(neverList, false)).toEqualTypeOf() - expectTypeOf(_.first(emptyList, false)).toEqualTypeOf() - expectTypeOf(_.first(filledList, false)).toEqualTypeOf() + + test('variable with readonly tuple', () => { + const emptyTuple = [] as const + const tuple = [1, 2, 3] as const + + expectTypeOf(_.first(emptyTuple)).toEqualTypeOf() + expectTypeOf(_.first(tuple)).toEqualTypeOf<1>() }) - test('return type with immutable variable', () => { - const neverList: never[] = [] as const - const emptyList: number[] = [] as const - const filledList = [1, 2, 3] as const - - expectTypeOf(_.first(neverList, false)).toBeBoolean() - // FIXME: Can this be narrowed to `boolean`? - expectTypeOf(_.first(emptyList, false)).toEqualTypeOf() - expectTypeOf(_.first(filledList, false)).toEqualTypeOf<1 | 2 | 3>() + + test('with default value', () => { + const emptyArray = [] as never[] + const emptyTuple = [] as const + const array = [1, 2, 3] + const tuple = [1, 2, 3] as const + + expectTypeOf(_.first(emptyArray, false)).toEqualTypeOf() + expectTypeOf(_.first(emptyTuple, false)).toEqualTypeOf() + expectTypeOf(_.first(array, false)).toEqualTypeOf() + expectTypeOf(_.first(tuple, false)).toEqualTypeOf<1>() }) }) diff --git a/tests/array/last.test-d.ts b/tests/array/last.test-d.ts index 44109e7d..8578490c 100644 --- a/tests/array/last.test-d.ts +++ b/tests/array/last.test-d.ts @@ -1,53 +1,40 @@ import * as _ from 'radashi' -describe('last types', () => { - test('return type with literal argument', () => { - expectTypeOf(_.last([])).toBeUndefined() - expectTypeOf(_.last([1, 2, 3])).toBeNumber() +describe('last', () => { + test('inlined array', () => { + expectTypeOf(_.last([])).toEqualTypeOf() + expectTypeOf(_.last([1, 2, 3])).toEqualTypeOf<3>() }) - test('return type with mutable variable', () => { - const neverList: never[] = [] - const emptyList: number[] = [] - const filledList = [1, 2, 3] - - expectTypeOf(_.last(neverList)).toEqualTypeOf() - expectTypeOf(_.last(emptyList)).toEqualTypeOf() - expectTypeOf(_.last(filledList)).toEqualTypeOf() - }) - test('return type with immutable variable', () => { - const neverList: never[] = [] as const - const emptyList: number[] = [] as const - const filledList = [1, 2, 3] as const - - expectTypeOf(_.last(neverList)).toBeUndefined() - // FIXME: Can this be narrowed to `undefined`? - expectTypeOf(_.last(emptyList)).toEqualTypeOf() - expectTypeOf(_.last(filledList)).toEqualTypeOf<1 | 2 | 3>() + + test('variable with empty array', () => { + const emptyArray = [] as never[] + + expectTypeOf(_.last(emptyArray)).toEqualTypeOf() }) -}) -describe('last types with default value', () => { - test('return type with literal argument', () => { - expectTypeOf(_.last([], false)).toBeBoolean() - expectTypeOf(_.last([1, 2, 3], false)).toBeNumber() + test('variable with mutable array', () => { + const array = [1, 2, 3] + + expectTypeOf(_.last(array)).toEqualTypeOf() }) - test('return type with mutable variable', () => { - const neverList: never[] = [] - const emptyList: number[] = [] - const filledList = [1, 2, 3] - - expectTypeOf(_.last(neverList, false)).toEqualTypeOf() - expectTypeOf(_.last(emptyList, false)).toEqualTypeOf() - expectTypeOf(_.last(filledList, false)).toEqualTypeOf() + + test('variable with readonly tuple', () => { + const emptyTuple = [] as const + const tuple = [1, 2, 3] as const + + expectTypeOf(_.last(emptyTuple)).toEqualTypeOf() + expectTypeOf(_.last(tuple)).toEqualTypeOf<3>() }) - test('return type with immutable variable', () => { - const neverList: never[] = [] as const - const emptyList: number[] = [] as const - const filledList = [1, 2, 3] as const - - expectTypeOf(_.last(neverList, false)).toBeBoolean() - // FIXME: Can this be narrowed to `boolean`? - expectTypeOf(_.last(emptyList, false)).toEqualTypeOf() - expectTypeOf(_.last(filledList, false)).toEqualTypeOf<1 | 2 | 3>() + + test('with default value', () => { + const emptyArray = [] as never[] + const emptyTuple = [] as const + const array = [1, 2, 3] + const tuple = [1, 2, 3] as const + + expectTypeOf(_.last(emptyArray, false)).toEqualTypeOf() + expectTypeOf(_.last(emptyTuple, false)).toEqualTypeOf() + expectTypeOf(_.last(array, false)).toEqualTypeOf() + expectTypeOf(_.last(tuple, false)).toEqualTypeOf<3>() }) })