From 0c87b7130d5afa1723524ee021d346fd59b45c7d Mon Sep 17 00:00:00 2001 From: harris-miller Date: Wed, 15 Nov 2023 23:18:34 -0700 Subject: [PATCH 01/16] add isNotEmpty typings --- types/isEmpty.d.ts | 3 +++ types/isNotEmpty.d.ts | 5 +++++ types/util/tools.d.ts | 10 ++++++++++ 3 files changed, 18 insertions(+) create mode 100644 types/isNotEmpty.d.ts diff --git a/types/isEmpty.d.ts b/types/isEmpty.d.ts index eb346158..405db351 100644 --- a/types/isEmpty.d.ts +++ b/types/isEmpty.d.ts @@ -1 +1,4 @@ +export function isEmpty(value: T[]): value is []; +export function isEmpty(value: T): value is ''; +export function isEmpty(value: T): value is {}; export function isEmpty(value: any): boolean; diff --git a/types/isNotEmpty.d.ts b/types/isNotEmpty.d.ts new file mode 100644 index 00000000..5325194b --- /dev/null +++ b/types/isNotEmpty.d.ts @@ -0,0 +1,5 @@ +import { NonEmptyArray, ReadOnlyNonEmptyArray } from './util/tools'; + +export function isNotEmpty(value: readonly T[]): value is ReadOnlyNonEmptyArray; +export function isNotEmpty(value: T[]): value is NonEmptyArray; +export function isNotEmpty(value: any): boolean; diff --git a/types/util/tools.d.ts b/types/util/tools.d.ts index 2dd46188..5f2962f2 100644 --- a/types/util/tools.d.ts +++ b/types/util/tools.d.ts @@ -508,3 +508,13 @@ export type WidenLiterals = * */ export type ElementOf = Type[number]; + +/** + * + */ +export type NonEmptyArray = [T, ...T[]]; + +/** + * + */ +export type ReadonlyNonEmptyArray = readonly [T, ...T[]]; From a2a644f922e104c2f264ed724e61797aef7f1d5a Mon Sep 17 00:00:00 2001 From: harris-miller Date: Wed, 15 Nov 2023 23:25:06 -0700 Subject: [PATCH 02/16] WIP --- types/head.d.ts | 4 +++- types/isNotEmpty.d.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/types/head.d.ts b/types/head.d.ts index 3bc1cf89..181b54a0 100644 --- a/types/head.d.ts +++ b/types/head.d.ts @@ -1,8 +1,10 @@ +// empty tuple - purposefully `never`. `head` should never work on an empty string +export function head(str: ''): never; // string export function head(str: string): string; // empty tuple - purposefully `never`. `head` should never work on tuple type with no length export function head(list: readonly []): never; -// non-empty tuple +// non-empty tuple - this can be a [T1, T2, T3] style tuple or a `NonEmptyArray` type that can be produced by `isNotEmpty` export function head(list: readonly [T1, ...TRest[]]): T1; // arrays, because these could be empty, they return `T | undefined` // this is no different than the tuple form since `T[]` can be empty at runtime diff --git a/types/isNotEmpty.d.ts b/types/isNotEmpty.d.ts index 5325194b..d8d807ee 100644 --- a/types/isNotEmpty.d.ts +++ b/types/isNotEmpty.d.ts @@ -1,5 +1,5 @@ -import { NonEmptyArray, ReadOnlyNonEmptyArray } from './util/tools'; +import { NonEmptyArray, ReadonlyNonEmptyArray } from './util/tools'; -export function isNotEmpty(value: readonly T[]): value is ReadOnlyNonEmptyArray; +export function isNotEmpty(value: readonly T[]): value is ReadonlyNonEmptyArray; export function isNotEmpty(value: T[]): value is NonEmptyArray; export function isNotEmpty(value: any): boolean; From 68d5cbe5cf74084a8d661baf3a5af2fed663f03e Mon Sep 17 00:00:00 2001 From: harris-miller Date: Thu, 16 Nov 2023 00:53:49 -0700 Subject: [PATCH 03/16] cool --- test/head.test.ts | 6 +++--- types/head.d.ts | 11 +++++------ types/isEmpty.d.ts | 3 --- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/test/head.test.ts b/test/head.test.ts index 41e36704..8de0af8d 100644 --- a/test/head.test.ts +++ b/test/head.test.ts @@ -1,10 +1,10 @@ -import { expectType } from 'tsd'; +import { expectType, expectError } from 'tsd'; import { head } from '../es'; +// empty string literal errors +expectError(head('')); // string always return string expectType(head('abc')); -// emptyString still returns type string. this is due to ramda's implementation `''.chartAt(0) => ''` -expectType(head('')); // array literals will read the type of the first entry expectType(head(['fi', 1, 'fum'])); diff --git a/types/head.d.ts b/types/head.d.ts index 181b54a0..e11c3886 100644 --- a/types/head.d.ts +++ b/types/head.d.ts @@ -1,11 +1,10 @@ -// empty tuple - purposefully `never`. `head` should never work on an empty string -export function head(str: ''): never; -// string -export function head(str: string): string; +// string - rejecting empty string literals +export function head(str: T extends '' ? never : T): string; // empty tuple - purposefully `never`. `head` should never work on tuple type with no length export function head(list: readonly []): never; // non-empty tuple - this can be a [T1, T2, T3] style tuple or a `NonEmptyArray` type that can be produced by `isNotEmpty` export function head(list: readonly [T1, ...TRest[]]): T1; -// arrays, because these could be empty, they return `T | undefined` +// arrays, because these could be empty, they return `T | undefined // this is no different than the tuple form since `T[]` can be empty at runtime -export function head(list: readonly T[]): T | undefined; +// rejects empty tuple +export function head(list: T extends readonly [] ? never : T): T extends (infer V)[] ? (V | undefined) : never; diff --git a/types/isEmpty.d.ts b/types/isEmpty.d.ts index 405db351..eb346158 100644 --- a/types/isEmpty.d.ts +++ b/types/isEmpty.d.ts @@ -1,4 +1 @@ -export function isEmpty(value: T[]): value is []; -export function isEmpty(value: T): value is ''; -export function isEmpty(value: T): value is {}; export function isEmpty(value: any): boolean; From 704b43ed79c62420b863fa688abbe5e1de22e0e2 Mon Sep 17 00:00:00 2001 From: harris-miller Date: Fri, 17 Nov 2023 23:27:54 -0700 Subject: [PATCH 04/16] updated types for head, need to add tests for isEmpty and isNotEmpty --- test/head.test.ts | 9 +++++++-- test/isEmpty.test.ts | 0 test/isNotEmpty.test.ts | 0 types/head.d.ts | 2 -- 4 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 test/isEmpty.test.ts create mode 100644 test/isNotEmpty.test.ts diff --git a/test/head.test.ts b/test/head.test.ts index 8de0af8d..543608d6 100644 --- a/test/head.test.ts +++ b/test/head.test.ts @@ -10,8 +10,13 @@ expectType(head('abc')); expectType(head(['fi', 1, 'fum'])); // but if the array is typed as an `Array or T[]`, then return type will be `T` expectType(head(['fi', 1, 'fum'] as Array)); -// empty array literals return never -expectType(head([])); +// empty array literals return undefined +expectType(head([])); +// empty tuple errors +const emptyTuple: [] = []; +expectError(head(emptyTuple)); +// as does `[] as const` +expectError(head([] as const)); // but if it is typed, it will be `T | undefined` expectType(head([] as number[])); // const tuples return the literal type of the first entry diff --git a/test/isEmpty.test.ts b/test/isEmpty.test.ts new file mode 100644 index 00000000..e69de29b diff --git a/test/isNotEmpty.test.ts b/test/isNotEmpty.test.ts new file mode 100644 index 00000000..e69de29b diff --git a/types/head.d.ts b/types/head.d.ts index e11c3886..86bd5bc7 100644 --- a/types/head.d.ts +++ b/types/head.d.ts @@ -1,7 +1,5 @@ // string - rejecting empty string literals export function head(str: T extends '' ? never : T): string; -// empty tuple - purposefully `never`. `head` should never work on tuple type with no length -export function head(list: readonly []): never; // non-empty tuple - this can be a [T1, T2, T3] style tuple or a `NonEmptyArray` type that can be produced by `isNotEmpty` export function head(list: readonly [T1, ...TRest[]]): T1; // arrays, because these could be empty, they return `T | undefined From eaaa649dc2ad88c7e4563ee6dc9c59235123ff11 Mon Sep 17 00:00:00 2001 From: harris-miller Date: Mon, 20 Nov 2023 01:56:29 -0700 Subject: [PATCH 05/16] some tests --- test/isEmpty.test.ts | 0 test/isNotEmpty.test.ts | 24 ++++++++++++++++++++++++ types/isNotEmpty.d.ts | 2 +- 3 files changed, 25 insertions(+), 1 deletion(-) delete mode 100644 test/isEmpty.test.ts diff --git a/test/isEmpty.test.ts b/test/isEmpty.test.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/test/isNotEmpty.test.ts b/test/isNotEmpty.test.ts index e69de29b..e036fc62 100644 --- a/test/isNotEmpty.test.ts +++ b/test/isNotEmpty.test.ts @@ -0,0 +1,24 @@ +import { expectType, expectError } from 'tsd'; + +// TODO: check this import to '../es' once this function actually exists in ramda +import { isNotEmpty } from '../types/isNotEmpty'; +import { ReadonlyNonEmptyArray, NonEmptyArray } from '../es'; + + +// test the type narrowing +const readonlyArr: readonly number[] = []; +if (isNotEmpty(readonlyArr)) { + expectType>(readonlyArr); +} + +// test the type narrowing +const arr: number[] = []; +if (isNotEmpty(arr)) { + expectType>(arr); +} + +// tuples fall through +const tuple: [number, string] = [1, '1']; +if (isNotEmpty(tuple)) { + expectType<[number, string]>(tuple); +} diff --git a/types/isNotEmpty.d.ts b/types/isNotEmpty.d.ts index d8d807ee..04b740b4 100644 --- a/types/isNotEmpty.d.ts +++ b/types/isNotEmpty.d.ts @@ -1,5 +1,5 @@ import { NonEmptyArray, ReadonlyNonEmptyArray } from './util/tools'; -export function isNotEmpty(value: readonly T[]): value is ReadonlyNonEmptyArray; export function isNotEmpty(value: T[]): value is NonEmptyArray; +export function isNotEmpty(value: readonly T[]): value is ReadonlyNonEmptyArray; export function isNotEmpty(value: any): boolean; From dd10cd50cfbfe93c639a16bf92e4a0c4b4d23201 Mon Sep 17 00:00:00 2001 From: harris-miller Date: Mon, 20 Nov 2023 02:03:01 -0700 Subject: [PATCH 06/16] additional tests --- test/head.test.ts | 1 + test/isNotEmpty.test.ts | 27 ++++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/test/head.test.ts b/test/head.test.ts index 543608d6..e88308eb 100644 --- a/test/head.test.ts +++ b/test/head.test.ts @@ -1,5 +1,6 @@ import { expectType, expectError } from 'tsd'; import { head } from '../es'; +import { isNotEmpty } from '../types/isNotEmpty'; // empty string literal errors expectError(head('')); diff --git a/test/isNotEmpty.test.ts b/test/isNotEmpty.test.ts index e036fc62..a78bbdb4 100644 --- a/test/isNotEmpty.test.ts +++ b/test/isNotEmpty.test.ts @@ -1,4 +1,4 @@ -import { expectType, expectError } from 'tsd'; +import { expectType } from 'tsd'; // TODO: check this import to '../es' once this function actually exists in ramda import { isNotEmpty } from '../types/isNotEmpty'; @@ -11,14 +11,35 @@ if (isNotEmpty(readonlyArr)) { expectType>(readonlyArr); } -// test the type narrowing +const readonlyArr2: readonly number[] = []; +if (!isNotEmpty(readonlyArr2)) { + // no-op +} else { + expectType>(readonlyArr2); +} + + const arr: number[] = []; if (isNotEmpty(arr)) { expectType>(arr); } -// tuples fall through +const arr2: number[] = []; +if (!isNotEmpty(arr2)) { +// no-op +} else { + expectType>(arr2); +} + + +// tuples retain their type const tuple: [number, string] = [1, '1']; if (isNotEmpty(tuple)) { expectType<[number, string]>(tuple); } + +// `as const` retain their type +const tuple2 = [1, 2, 3] as const; +if (isNotEmpty(tuple2)) { + expectType(tuple2); +} From 05d4a4cdb86476e542aa32226e93f061403a997f Mon Sep 17 00:00:00 2001 From: harris-miller Date: Mon, 20 Nov 2023 02:05:37 -0700 Subject: [PATCH 07/16] tests for head --- test/head.test.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/head.test.ts b/test/head.test.ts index e88308eb..4356f0e5 100644 --- a/test/head.test.ts +++ b/test/head.test.ts @@ -29,3 +29,30 @@ expectType(head(['10', 10] as [string, number])); // typed arrays return `T | undefined` expectType(head([10, 'ten'] as Array)); expectType(head(['10', 10] as Array)); + +// cross function testing with isNotEmpty +// test the type narrowing +const readonlyArr: readonly number[] = []; +if (isNotEmpty(readonlyArr)) { + expectType(head(readonlyArr)); +} + +const readonlyArr2: readonly number[] = []; +if (!isNotEmpty(readonlyArr2)) { + // no-op +} else { + expectType(head(readonlyArr2)); +} + + +const arr: number[] = []; +if (isNotEmpty(arr)) { + expectType(head(arr)); +} + +const arr2: number[] = []; +if (!isNotEmpty(arr2)) { +// no-op +} else { + expectType(head(arr2)); +} From 312a066ffca2c7a2b1df38a8d192d7e419bcf11d Mon Sep 17 00:00:00 2001 From: harris-miller Date: Mon, 20 Nov 2023 02:27:08 -0700 Subject: [PATCH 08/16] update types for last and add tests --- test/last.test.ts | 58 +++++++++++++++++++++++++++++++++++++++++++++++ types/head.d.ts | 2 +- types/last.d.ts | 15 +++++++++--- 3 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 test/last.test.ts diff --git a/test/last.test.ts b/test/last.test.ts new file mode 100644 index 00000000..2bb54d44 --- /dev/null +++ b/test/last.test.ts @@ -0,0 +1,58 @@ +import { expectType, expectError } from 'tsd'; +import { last } from '../es'; +import { isNotEmpty } from '../types/isNotEmpty'; + +// empty string literal errors +expectError(last('')); +// string always return string +expectType(last('abc')); + +// array literals will read the type of the first entry +expectType(last(['fi', 1, 'fum'])); +// but if the array is typed as an `Array or T[]`, then return type will be `T` +expectType(last(['fi', 1, 'fum'] as Array)); +// empty array literals return undefined +expectType(last([])); +// empty tuple errors +const emptyTuple: [] = []; +expectError(last(emptyTuple)); +// as does `[] as const` +expectError(last([] as const)); +// but if it is typed, it will be `T | undefined` +expectType(last([] as number[])); +// const tuples return the literal type of the first entry +expectType<'ten'>(last([10, 'ten'] as const)); +expectType<10>(last(['10', 10] as const)); +// typed tuples return the underlying type +expectType(last([true, 10, 'ten'] as [boolean, number, string])); +expectType(last([false, '10', 10] as [boolean, string, number])); +// typed arrays return `T | undefined` +expectType(last([10, 'ten'] as Array)); +expectType(last(['10', 10] as Array)); + +// cross function testing with isNotEmpty +// test the type narrowing +const readonlyArr: readonly number[] = []; +if (isNotEmpty(readonlyArr)) { + expectType(last(readonlyArr)); +} + +const readonlyArr2: readonly number[] = []; +if (!isNotEmpty(readonlyArr2)) { + // no-op +} else { + expectType(last(readonlyArr2)); +} + + +const arr: number[] = []; +if (isNotEmpty(arr)) { + expectType(last(arr)); +} + +const arr2: number[] = []; +if (!isNotEmpty(arr2)) { +// no-op +} else { + expectType(last(arr2)); +} diff --git a/types/head.d.ts b/types/head.d.ts index 86bd5bc7..dd837b2c 100644 --- a/types/head.d.ts +++ b/types/head.d.ts @@ -1,7 +1,7 @@ // string - rejecting empty string literals export function head(str: T extends '' ? never : T): string; // non-empty tuple - this can be a [T1, T2, T3] style tuple or a `NonEmptyArray` type that can be produced by `isNotEmpty` -export function head(list: readonly [T1, ...TRest[]]): T1; +export function head(list: readonly [T, ...any[]]): T; // arrays, because these could be empty, they return `T | undefined // this is no different than the tuple form since `T[]` can be empty at runtime // rejects empty tuple diff --git a/types/last.d.ts b/types/last.d.ts index 6b0036fa..dd95ca91 100644 --- a/types/last.d.ts +++ b/types/last.d.ts @@ -1,3 +1,12 @@ -export function last(str: string): string; -export function last(list: readonly []): undefined; -export function last(list: readonly T[]): T | undefined; +import { ReadonlyNonEmptyArray } from '../es'; + +// string - rejecting empty string literals +export function last(str: T extends '' ? never : T): string; +// non-empty tuple - this can be a [T1, T2, T3] style tuple or a `NonEmptyArray` type that can be produced by `isNotEmpty` +export function last(list: readonly [...any[], T]): T; +// non-empty arrays, these types don't fall into the non-empty overload, Readonly here catches regular arrays too +export function last(list: ReadonlyNonEmptyArray): T; +// arrays, because these could be empty, they return `T | undefined +// this is no different than the tuple form since `T[]` can be empty at runtime +// rejects empty tuple +export function last(list: T extends readonly [] ? never : T): T extends (infer V)[] ? (V | undefined) : never; From 598fdd6adac5d2b824e67660c6e23b7e3dcb92be Mon Sep 17 00:00:00 2001 From: harris-miller Date: Mon, 20 Nov 2023 02:40:04 -0700 Subject: [PATCH 09/16] updated init and tail typings --- types/init.d.ts | 10 +++++++++- types/tail.d.ts | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/types/init.d.ts b/types/init.d.ts index 61209676..cdebab8f 100644 --- a/types/init.d.ts +++ b/types/init.d.ts @@ -1,2 +1,10 @@ +// string export function init(list: string): string; -export function init(list: readonly T[]): T[]; +// empty tuple - purposefully `never, They literally have no init +export function tail(list: readonly []): never; +// length=1 tuples also return `never`. They literally have no init +export function init(list: readonly [T]): never; +// non-empty tuples and array +// `infer Init` only works on types like `readonly [1, '2', 3]` where you will get back `['2', 3]` +// else, if the type is `string[]`, you'll get back `string[]` +export function init(list: T): T extends readonly [...infer Init, any] ? Init : T; diff --git a/types/tail.d.ts b/types/tail.d.ts index f15ae378..6e1a49dc 100644 --- a/types/tail.d.ts +++ b/types/tail.d.ts @@ -5,6 +5,6 @@ export function tail(list: readonly []): never; // length=1 tuples also return `never`. They literally have no tail export function tail(list: readonly [T]): never; // non-empty tuples and array -// `infer Rest` only works on types like `readonly [1, '2', 3]` where you will get back `['2', 3]` +// `infer Tail` only works on types like `readonly [1, '2', 3]` where you will get back `['2', 3]` // else, if the type is `string[]`, you'll get back `string[]` -export function tail(tuple: T): T extends readonly [any, ...infer Rest] ? Rest : T; +export function tail(list: T): T extends readonly [any, ...infer Tail] ? Tail : T; From 5ba4da525af03bdb55c9f0a7634bc9f2da752a67 Mon Sep 17 00:00:00 2001 From: harris-miller Date: Mon, 20 Nov 2023 02:51:58 -0700 Subject: [PATCH 10/16] tests for init --- test/init.test.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 test/init.test.ts diff --git a/test/init.test.ts b/test/init.test.ts new file mode 100644 index 00000000..ee83ba49 --- /dev/null +++ b/test/init.test.ts @@ -0,0 +1,24 @@ +import { expectType } from 'tsd'; +import { init } from '../es'; + +// string always return string +expectType(init('abc')); +// emptyString still returns type string. this is due to `''.chartAt(0) => ''` +expectType(init('')); + +// array literals will read the first type correctly +expectType<[string, number]>(init(['fi', 1, 'fum'])); +// but if the array is typed as an `Array or T[]`, then return type will be `T` +expectType>(init(['fi', 1, 'fum'] as Array)); +// empty array literals return never +expectType(init([])); +// but if it is typed, it will be `number[]` +expectType(init([] as number[])); +// single entry tuples return never, since they literally have no init +expectType(init([10] as const)); +// tuples return the example type of the input tuple minus the first entry +expectType<[10, '10']>(init([10, '10', 10] as const)); +expectType<['10', 10]>(init(['10', 10, '10'] as const)); +// typed arrays return the same type +expectType>(init([10, 'ten'] as Array)); +expectType>(init(['10', 10] as Array)); From b6b42f5cc5112a6730d463ec5cd4f166a48c2126 Mon Sep 17 00:00:00 2001 From: harris-miller Date: Tue, 2 Apr 2024 01:24:29 -0600 Subject: [PATCH 11/16] turns out dont need special handling for init/tail empty or single tuples, the base def is smart enough to handle them --- test/init.test.ts | 4 ++-- test/tail.test.ts | 4 ++-- types/init.d.ts | 5 ----- types/tail.d.ts | 4 ---- 4 files changed, 4 insertions(+), 13 deletions(-) diff --git a/test/init.test.ts b/test/init.test.ts index ee83ba49..af41b4da 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -11,11 +11,11 @@ expectType<[string, number]>(init(['fi', 1, 'fum'])); // but if the array is typed as an `Array or T[]`, then return type will be `T` expectType>(init(['fi', 1, 'fum'] as Array)); // empty array literals return never -expectType(init([])); +expectType(init([])); // but if it is typed, it will be `number[]` expectType(init([] as number[])); // single entry tuples return never, since they literally have no init -expectType(init([10] as const)); +expectType<[]>(init([10] as const)); // tuples return the example type of the input tuple minus the first entry expectType<[10, '10']>(init([10, '10', 10] as const)); expectType<['10', 10]>(init(['10', 10, '10'] as const)); diff --git a/test/tail.test.ts b/test/tail.test.ts index 8a1e3569..7754a011 100644 --- a/test/tail.test.ts +++ b/test/tail.test.ts @@ -11,11 +11,11 @@ expectType<[number, string]>(tail(['fi', 1, 'fum'])); // but if the array is typed as an `Array or T[]`, then return type will be `T` expectType>(tail(['fi', 1, 'fum'] as Array)); // empty array literals return never -expectType(tail([])); +expectType(tail([])); // but if it is typed, it will be `number[]` expectType(tail([] as number[])); // single entry tuples return never, since they literally have no tail -expectType(tail([10] as const)); +expectType<[]>(tail([10] as const)); // tuples return the example type of the input tuple minus the first entry expectType<['10', 10]>(tail([10, '10', 10] as const)); expectType<[10, '10']>(tail(['10', 10, '10'] as const)); diff --git a/types/init.d.ts b/types/init.d.ts index cdebab8f..b4bbd478 100644 --- a/types/init.d.ts +++ b/types/init.d.ts @@ -1,10 +1,5 @@ // string export function init(list: string): string; -// empty tuple - purposefully `never, They literally have no init -export function tail(list: readonly []): never; -// length=1 tuples also return `never`. They literally have no init -export function init(list: readonly [T]): never; -// non-empty tuples and array // `infer Init` only works on types like `readonly [1, '2', 3]` where you will get back `['2', 3]` // else, if the type is `string[]`, you'll get back `string[]` export function init(list: T): T extends readonly [...infer Init, any] ? Init : T; diff --git a/types/tail.d.ts b/types/tail.d.ts index 6e1a49dc..c4417a57 100644 --- a/types/tail.d.ts +++ b/types/tail.d.ts @@ -1,9 +1,5 @@ // string export function tail(list: string): string; -// empty tuple - purposefully `never, They literally have no tail -export function tail(list: readonly []): never; -// length=1 tuples also return `never`. They literally have no tail -export function tail(list: readonly [T]): never; // non-empty tuples and array // `infer Tail` only works on types like `readonly [1, '2', 3]` where you will get back `['2', 3]` // else, if the type is `string[]`, you'll get back `string[]` From dd30f77388c9a5ba012a5cf000b80afe96324f50 Mon Sep 17 00:00:00 2001 From: harris-miller Date: Tue, 2 Apr 2024 01:24:50 -0600 Subject: [PATCH 12/16] update a comment --- types/tail.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/types/tail.d.ts b/types/tail.d.ts index c4417a57..c7777a58 100644 --- a/types/tail.d.ts +++ b/types/tail.d.ts @@ -1,6 +1,5 @@ // string export function tail(list: string): string; -// non-empty tuples and array // `infer Tail` only works on types like `readonly [1, '2', 3]` where you will get back `['2', 3]` // else, if the type is `string[]`, you'll get back `string[]` export function tail(list: T): T extends readonly [any, ...infer Tail] ? Tail : T; From ee8a51cf807ad08c1f99452b94935fbab9c29ce0 Mon Sep 17 00:00:00 2001 From: harris-miller Date: Tue, 2 Apr 2024 01:37:05 -0600 Subject: [PATCH 13/16] special handle NonEmptyArray's for init --- test/init.test.ts | 12 ++++++++++++ test/tail.test.ts | 12 ++++++++++++ types/init.d.ts | 8 +++++++- types/util/tools.d.ts | 6 ++++-- 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/test/init.test.ts b/test/init.test.ts index af41b4da..7b118d29 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -1,4 +1,7 @@ import { expectType } from 'tsd'; + +// TODO: check this import to '../es' once this function actually exists in ramda +import { isNotEmpty } from '../types/isNotEmpty'; import { init } from '../es'; // string always return string @@ -22,3 +25,12 @@ expectType<['10', 10]>(init(['10', 10, '10'] as const)); // typed arrays return the same type expectType>(init([10, 'ten'] as Array)); expectType>(init(['10', 10] as Array)); + +// works correctly with isNotEmpty +const arr = [1, 2, 3, 4]; + +expectType(init(arr)); + +if (isNotEmpty(arr)) { + expectType(init(arr)); +} diff --git a/test/tail.test.ts b/test/tail.test.ts index 7754a011..3113d424 100644 --- a/test/tail.test.ts +++ b/test/tail.test.ts @@ -1,4 +1,7 @@ import { expectType } from 'tsd'; + +// TODO: check this import to '../es' once this function actually exists in ramda +import { isNotEmpty } from '../types/isNotEmpty'; import { tail } from '../es'; // string always return string @@ -22,3 +25,12 @@ expectType<[10, '10']>(tail(['10', 10, '10'] as const)); // typed arrays return the same type expectType>(tail([10, 'ten'] as Array)); expectType>(tail(['10', 10] as Array)); + +// works correctly with isNotEmpty +const arr = [1, 2, 3, 4]; + +expectType(tail(arr)); + +if (isNotEmpty(arr)) { + expectType(tail(arr)); +} diff --git a/types/init.d.ts b/types/init.d.ts index b4bbd478..96765f10 100644 --- a/types/init.d.ts +++ b/types/init.d.ts @@ -1,5 +1,11 @@ +import { ReadonlyNonEmptyArray, NonEmptyArray } from '../es'; + // string export function init(list: string): string; // `infer Init` only works on types like `readonly [1, '2', 3]` where you will get back `['2', 3]` // else, if the type is `string[]`, you'll get back `string[]` -export function init(list: T): T extends readonly [...infer Init, any] ? Init : T; +export function init(list: T): +T extends readonly [...infer Init, any] ? Init : + T extends ReadonlyNonEmptyArray ? A[] : + T extends NonEmptyArray ? A[] : T; + diff --git a/types/util/tools.d.ts b/types/util/tools.d.ts index 5f2962f2..d73166ef 100644 --- a/types/util/tools.d.ts +++ b/types/util/tools.d.ts @@ -510,11 +510,13 @@ export type WidenLiterals = export type ElementOf = Type[number]; /** - * + * A clever way to represent a non-empty array + * */ export type NonEmptyArray = [T, ...T[]]; /** - * + * A clever way to represent a readonly non-empty array + * */ export type ReadonlyNonEmptyArray = readonly [T, ...T[]]; From 00e166ae41031367694576b07315d22e84c30760 Mon Sep 17 00:00:00 2001 From: harris-miller Date: Wed, 1 May 2024 18:35:14 -0600 Subject: [PATCH 14/16] bump all devDeps target=minor, for ramda@0.30.0 and the others because why not --- package-lock.json | 44 ++++++++++++++++++++++---------------------- package.json | 4 ++-- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 10a710ec..d9a40d9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,9 +17,9 @@ "dox": "^1.0.0", "eslint": "^8.50.0", "eslint-plugin-import": "^2.28.1", - "ramda": "^0.29.1", + "ramda": "^0.30.0", "rimraf": "^5.0.5", - "tsd": "^0.29.0", + "tsd": "^0.31.0", "typescript": "^5.2.2", "xyz": "^4.0.0" } @@ -350,9 +350,9 @@ "dev": true }, "node_modules/@tsd/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-VtjHPAKJqLJoHHKBDNofzvQB2+ZVxjXU/Gw6INAS9aINLQYVsxfzrQ2s84huCeYWZRTtrr7R0J7XgpZHjNwBCw==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-saiCxzHRhUrRxQV2JhH580aQUZiKQUXI38FcAcikcfOomAil4G4lxT0RfrrKywoAYP/rqAdYXYmNRLppcd+hQQ==", "dev": true, "engines": { "node": ">=14.17" @@ -3115,9 +3115,9 @@ } }, "node_modules/ramda": { - "version": "0.29.1", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.1.tgz", - "integrity": "sha512-OfxIeWzd4xdUNxlWhgFazxsA/nl3mS4/jGZI5n00uWOoSSFRhC1b6gl6xvmzUamgmqELraWp0J/qqVlXYPDPyA==", + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.0.tgz", + "integrity": "sha512-13Y0iMhIQuAm/wNGBL/9HEqIfRGmNmjKnTPlKWfA9f7dnDkr8d45wQ+S7+ZLh/Pq9PdcGxkqKUEA7ySu1QSd9Q==", "dev": true, "funding": { "type": "opencollective", @@ -3689,12 +3689,12 @@ } }, "node_modules/tsd": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.29.0.tgz", - "integrity": "sha512-5B7jbTj+XLMg6rb9sXRBGwzv7h8KJlGOkTHxY63eWpZJiQ5vJbXEjL0u7JkIxwi5EsrRE1kRVUWmy6buK/ii8A==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.31.0.tgz", + "integrity": "sha512-yjBiQ5n8OMv/IZOuhDjBy0ZLCoJ7rky/RxRh5W4sJ0oNNCU/kf6s3puPAkGNi59PptDdkcpUm+RsKSdjR2YbNg==", "dev": true, "dependencies": { - "@tsd/typescript": "~5.2.2", + "@tsd/typescript": "~5.4.3", "eslint-formatter-pretty": "^4.1.0", "globby": "^11.0.1", "jest-diff": "^29.0.3", @@ -4284,9 +4284,9 @@ "dev": true }, "@tsd/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-VtjHPAKJqLJoHHKBDNofzvQB2+ZVxjXU/Gw6INAS9aINLQYVsxfzrQ2s84huCeYWZRTtrr7R0J7XgpZHjNwBCw==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/@tsd/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-saiCxzHRhUrRxQV2JhH580aQUZiKQUXI38FcAcikcfOomAil4G4lxT0RfrrKywoAYP/rqAdYXYmNRLppcd+hQQ==", "dev": true }, "@types/eslint": { @@ -6263,9 +6263,9 @@ "dev": true }, "ramda": { - "version": "0.29.1", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.1.tgz", - "integrity": "sha512-OfxIeWzd4xdUNxlWhgFazxsA/nl3mS4/jGZI5n00uWOoSSFRhC1b6gl6xvmzUamgmqELraWp0J/qqVlXYPDPyA==", + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.0.tgz", + "integrity": "sha512-13Y0iMhIQuAm/wNGBL/9HEqIfRGmNmjKnTPlKWfA9f7dnDkr8d45wQ+S7+ZLh/Pq9PdcGxkqKUEA7ySu1QSd9Q==", "dev": true }, "react-is": { @@ -6677,12 +6677,12 @@ } }, "tsd": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.29.0.tgz", - "integrity": "sha512-5B7jbTj+XLMg6rb9sXRBGwzv7h8KJlGOkTHxY63eWpZJiQ5vJbXEjL0u7JkIxwi5EsrRE1kRVUWmy6buK/ii8A==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.31.0.tgz", + "integrity": "sha512-yjBiQ5n8OMv/IZOuhDjBy0ZLCoJ7rky/RxRh5W4sJ0oNNCU/kf6s3puPAkGNi59PptDdkcpUm+RsKSdjR2YbNg==", "dev": true, "requires": { - "@tsd/typescript": "~5.2.2", + "@tsd/typescript": "~5.4.3", "eslint-formatter-pretty": "^4.1.0", "globby": "^11.0.1", "jest-diff": "^29.0.3", diff --git a/package.json b/package.json index a4909cfe..809bb67c 100644 --- a/package.json +++ b/package.json @@ -67,9 +67,9 @@ "dox": "^1.0.0", "eslint": "^8.50.0", "eslint-plugin-import": "^2.28.1", - "ramda": "^0.29.1", + "ramda": "^0.30.0", "rimraf": "^5.0.5", - "tsd": "^0.29.0", + "tsd": "^0.31.0", "typescript": "^5.2.2", "xyz": "^4.0.0" } From 823c8457ba1e1f5d198a58c999abc6a090f3ab3a Mon Sep 17 00:00:00 2001 From: harris-miller Date: Wed, 1 May 2024 20:05:15 -0600 Subject: [PATCH 15/16] bit less strict on last for empty string and tuple --- test/last.test.ts | 28 +++++++++++++++------------- types/last.d.ts | 6 ++---- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/test/last.test.ts b/test/last.test.ts index 2bb54d44..ad85ba81 100644 --- a/test/last.test.ts +++ b/test/last.test.ts @@ -1,11 +1,11 @@ -import { expectType, expectError } from 'tsd'; +import { expectType } from 'tsd'; import { last } from '../es'; import { isNotEmpty } from '../types/isNotEmpty'; -// empty string literal errors -expectError(last('')); +// strings always return `string | undefined`, can't determine "emptiness" like you can with arrays +expectType(last('')); // string always return string -expectType(last('abc')); +expectType(last('abc')); // array literals will read the type of the first entry expectType(last(['fi', 1, 'fum'])); @@ -13,19 +13,21 @@ expectType(last(['fi', 1, 'fum'])); expectType(last(['fi', 1, 'fum'] as Array)); // empty array literals return undefined expectType(last([])); -// empty tuple errors -const emptyTuple: [] = []; -expectError(last(emptyTuple)); -// as does `[] as const` -expectError(last([] as const)); -// but if it is typed, it will be `T | undefined` + +// typed empty array will be `T | undefined` expectType(last([] as number[])); -// const tuples return the literal type of the first entry +// as will a typed populated array +expectType(last([1, 2, 3] as number[])); + +// const tuples return the literal type of the last entry expectType<'ten'>(last([10, 'ten'] as const)); expectType<10>(last(['10', 10] as const)); -// typed tuples return the underlying type +// typed tuples return the type of the last element expectType(last([true, 10, 'ten'] as [boolean, number, string])); expectType(last([false, '10', 10] as [boolean, string, number])); +// typed empty tuple returns undefined +expectType(last([] as [])); + // typed arrays return `T | undefined` expectType(last([10, 'ten'] as Array)); expectType(last(['10', 10] as Array)); @@ -52,7 +54,7 @@ if (isNotEmpty(arr)) { const arr2: number[] = []; if (!isNotEmpty(arr2)) { -// no-op + // no-op } else { expectType(last(arr2)); } diff --git a/types/last.d.ts b/types/last.d.ts index dd95ca91..22cb1566 100644 --- a/types/last.d.ts +++ b/types/last.d.ts @@ -1,12 +1,10 @@ import { ReadonlyNonEmptyArray } from '../es'; -// string - rejecting empty string literals -export function last(str: T extends '' ? never : T): string; +export function last(str: string): string | undefined; // non-empty tuple - this can be a [T1, T2, T3] style tuple or a `NonEmptyArray` type that can be produced by `isNotEmpty` export function last(list: readonly [...any[], T]): T; // non-empty arrays, these types don't fall into the non-empty overload, Readonly here catches regular arrays too export function last(list: ReadonlyNonEmptyArray): T; // arrays, because these could be empty, they return `T | undefined // this is no different than the tuple form since `T[]` can be empty at runtime -// rejects empty tuple -export function last(list: T extends readonly [] ? never : T): T extends (infer V)[] ? (V | undefined) : never; +export function last(list: readonly T[]): T | undefined; From 6d841c225a6f2579cce1adea2a0dd657ea2fea4d Mon Sep 17 00:00:00 2001 From: harris-miller Date: Wed, 1 May 2024 20:23:50 -0600 Subject: [PATCH 16/16] simplify and correct all tests --- test/head.test.ts | 38 +++++++++++++++++++++----------------- test/last.test.ts | 8 +++++--- types/head.d.ts | 13 +++++++------ types/isNotEmpty.d.ts | 1 + types/last.d.ts | 5 ++--- 5 files changed, 36 insertions(+), 29 deletions(-) diff --git a/test/head.test.ts b/test/head.test.ts index 4356f0e5..5a50245d 100644 --- a/test/head.test.ts +++ b/test/head.test.ts @@ -1,34 +1,38 @@ -import { expectType, expectError } from 'tsd'; +import { expectType } from 'tsd'; import { head } from '../es'; import { isNotEmpty } from '../types/isNotEmpty'; -// empty string literal errors -expectError(head('')); +// strings always return `string | undefined`, can't determine "emptiness" like you can with arrays +expectType(head('')); // string always return string -expectType(head('abc')); +expectType(head('abc')); // array literals will read the type of the first entry expectType(head(['fi', 1, 'fum'])); -// but if the array is typed as an `Array or T[]`, then return type will be `T` -expectType(head(['fi', 1, 'fum'] as Array)); // empty array literals return undefined expectType(head([])); -// empty tuple errors -const emptyTuple: [] = []; -expectError(head(emptyTuple)); -// as does `[] as const` -expectError(head([] as const)); -// but if it is typed, it will be `T | undefined` + +// typed empty array will be `T | undefined` expectType(head([] as number[])); -// const tuples return the literal type of the first entry +// as will a typed populated array +expectType(head([1, 2, 3] as number[])); + +// const tuples return the literal type of the last entry +expectType<10>(head([10] as const)); expectType<10>(head([10, 'ten'] as const)); expectType<'10'>(head(['10', 10] as const)); -// typed tuples return the underlying type -expectType(head([10, 'ten'] as [number, string])); -expectType(head(['10', 10] as [string, number])); +// typed tuples return the type of the last element +expectType(head([true] as [boolean])); +expectType(head([10, 'ten', true] as [number, string, boolean])); +expectType(head(['10', 10, false] as [string, number, boolean])); +// typed empty tuple returns undefined, this is expected because there is no `T` here +expectType(head([] as [])); + // typed arrays return `T | undefined` expectType(head([10, 'ten'] as Array)); expectType(head(['10', 10] as Array)); +expectType(head([10, 'ten'] as ReadonlyArray)); +expectType(head(['10', 10] as ReadonlyArray)); // cross function testing with isNotEmpty // test the type narrowing @@ -52,7 +56,7 @@ if (isNotEmpty(arr)) { const arr2: number[] = []; if (!isNotEmpty(arr2)) { -// no-op + // no-op } else { expectType(head(arr2)); } diff --git a/test/last.test.ts b/test/last.test.ts index ad85ba81..1010f4c3 100644 --- a/test/last.test.ts +++ b/test/last.test.ts @@ -9,8 +9,6 @@ expectType(last('abc')); // array literals will read the type of the first entry expectType(last(['fi', 1, 'fum'])); -// but if the array is typed as an `Array or T[]`, then return type will be `T` -expectType(last(['fi', 1, 'fum'] as Array)); // empty array literals return undefined expectType(last([])); @@ -20,17 +18,21 @@ expectType(last([] as number[])); expectType(last([1, 2, 3] as number[])); // const tuples return the literal type of the last entry +expectType<10>(last([10] as const)); expectType<'ten'>(last([10, 'ten'] as const)); expectType<10>(last(['10', 10] as const)); // typed tuples return the type of the last element +expectType(last([true] as [boolean])); expectType(last([true, 10, 'ten'] as [boolean, number, string])); expectType(last([false, '10', 10] as [boolean, string, number])); -// typed empty tuple returns undefined +// typed empty tuple returns undefined, this is expected because there is no `T` here expectType(last([] as [])); // typed arrays return `T | undefined` expectType(last([10, 'ten'] as Array)); expectType(last(['10', 10] as Array)); +expectType(last([10, 'ten'] as ReadonlyArray)); +expectType(last(['10', 10] as ReadonlyArray)); // cross function testing with isNotEmpty // test the type narrowing diff --git a/types/head.d.ts b/types/head.d.ts index dd837b2c..858e107c 100644 --- a/types/head.d.ts +++ b/types/head.d.ts @@ -1,8 +1,9 @@ -// string - rejecting empty string literals -export function head(str: T extends '' ? never : T): string; -// non-empty tuple - this can be a [T1, T2, T3] style tuple or a `NonEmptyArray` type that can be produced by `isNotEmpty` +import { ReadonlyNonEmptyArray } from '../es'; + +export function head(str: string): string | undefined; +// non-empty tuple - Readonly here catches regular tuples too export function head(list: readonly [T, ...any[]]): T; +// non-empty arrays - Readonly here catches regular arrays too +export function head(list: ReadonlyNonEmptyArray): T; // arrays, because these could be empty, they return `T | undefined -// this is no different than the tuple form since `T[]` can be empty at runtime -// rejects empty tuple -export function head(list: T extends readonly [] ? never : T): T extends (infer V)[] ? (V | undefined) : never; +export function head(list: readonly T[]): T | undefined; diff --git a/types/isNotEmpty.d.ts b/types/isNotEmpty.d.ts index 04b740b4..e7f66a57 100644 --- a/types/isNotEmpty.d.ts +++ b/types/isNotEmpty.d.ts @@ -1,5 +1,6 @@ import { NonEmptyArray, ReadonlyNonEmptyArray } from './util/tools'; +// array has to come first, because readonly T[] falls through export function isNotEmpty(value: T[]): value is NonEmptyArray; export function isNotEmpty(value: readonly T[]): value is ReadonlyNonEmptyArray; export function isNotEmpty(value: any): boolean; diff --git a/types/last.d.ts b/types/last.d.ts index 22cb1566..e3bbab29 100644 --- a/types/last.d.ts +++ b/types/last.d.ts @@ -1,10 +1,9 @@ import { ReadonlyNonEmptyArray } from '../es'; export function last(str: string): string | undefined; -// non-empty tuple - this can be a [T1, T2, T3] style tuple or a `NonEmptyArray` type that can be produced by `isNotEmpty` +// non-empty tuple - Readonly here catches regular tuples too export function last(list: readonly [...any[], T]): T; -// non-empty arrays, these types don't fall into the non-empty overload, Readonly here catches regular arrays too +// non-empty arrays - Readonly here catches regular arrays too export function last(list: ReadonlyNonEmptyArray): T; // arrays, because these could be empty, they return `T | undefined -// this is no different than the tuple form since `T[]` can be empty at runtime export function last(list: readonly T[]): T | undefined;