diff --git a/package-lock.json b/package-lock.json index 10a710e..d9a40d9 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 a4909cf..809bb67 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" } diff --git a/test/head.test.ts b/test/head.test.ts index 41e3670..5a50245 100644 --- a/test/head.test.ts +++ b/test/head.test.ts @@ -1,25 +1,62 @@ import { expectType } from 'tsd'; import { head } from '../es'; +import { isNotEmpty } from '../types/isNotEmpty'; +// strings always return `string | undefined`, can't determine "emptiness" like you can with arrays +expectType(head('')); // string always return string -expectType(head('abc')); -// emptyString still returns type string. this is due to ramda's implementation `''.chartAt(0) => ''` -expectType(head('')); +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 never -expectType(head([])); -// but if it is typed, it will be `T | undefined` +// empty array literals return undefined +expectType(head([])); + +// 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 +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)); +} diff --git a/test/init.test.ts b/test/init.test.ts new file mode 100644 index 0000000..7b118d2 --- /dev/null +++ b/test/init.test.ts @@ -0,0 +1,36 @@ +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 +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)); + +// works correctly with isNotEmpty +const arr = [1, 2, 3, 4]; + +expectType(init(arr)); + +if (isNotEmpty(arr)) { + expectType(init(arr)); +} diff --git a/test/isNotEmpty.test.ts b/test/isNotEmpty.test.ts new file mode 100644 index 0000000..a78bbdb --- /dev/null +++ b/test/isNotEmpty.test.ts @@ -0,0 +1,45 @@ +import { expectType } 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); +} + +const readonlyArr2: readonly number[] = []; +if (!isNotEmpty(readonlyArr2)) { + // no-op +} else { + expectType>(readonlyArr2); +} + + +const arr: number[] = []; +if (isNotEmpty(arr)) { + expectType>(arr); +} + +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); +} diff --git a/test/last.test.ts b/test/last.test.ts new file mode 100644 index 0000000..1010f4c --- /dev/null +++ b/test/last.test.ts @@ -0,0 +1,62 @@ +import { expectType } from 'tsd'; +import { last } from '../es'; +import { isNotEmpty } from '../types/isNotEmpty'; + +// strings always return `string | undefined`, can't determine "emptiness" like you can with arrays +expectType(last('')); +// string always return string +expectType(last('abc')); + +// array literals will read the type of the first entry +expectType(last(['fi', 1, 'fum'])); +// empty array literals return undefined +expectType(last([])); + +// typed empty array will be `T | undefined` +expectType(last([] as number[])); +// as will a typed populated array +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, 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 +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/test/tail.test.ts b/test/tail.test.ts index 8a1e356..3113d42 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 @@ -11,14 +14,23 @@ 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)); // 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/head.d.ts b/types/head.d.ts index 3bc1cf8..858e107 100644 --- a/types/head.d.ts +++ b/types/head.d.ts @@ -1,9 +1,9 @@ -// 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 -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 +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 export function head(list: readonly T[]): T | undefined; diff --git a/types/init.d.ts b/types/init.d.ts index 6120967..96765f1 100644 --- a/types/init.d.ts +++ b/types/init.d.ts @@ -1,2 +1,11 @@ +import { ReadonlyNonEmptyArray, NonEmptyArray } from '../es'; + +// string export function init(list: string): string; -export function init(list: readonly T[]): T[]; +// `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 extends ReadonlyNonEmptyArray ? A[] : + T extends NonEmptyArray ? A[] : T; + diff --git a/types/isNotEmpty.d.ts b/types/isNotEmpty.d.ts new file mode 100644 index 0000000..e7f66a5 --- /dev/null +++ b/types/isNotEmpty.d.ts @@ -0,0 +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 6b0036f..e3bbab2 100644 --- a/types/last.d.ts +++ b/types/last.d.ts @@ -1,3 +1,9 @@ -export function last(str: string): string; -export function last(list: readonly []): undefined; -export function last(list: readonly T[]): T | undefined; +import { ReadonlyNonEmptyArray } from '../es'; + +export function last(str: string): string | undefined; +// non-empty tuple - Readonly here catches regular tuples too +export function last(list: readonly [...any[], T]): T; +// 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 +export function last(list: readonly T[]): T | undefined; diff --git a/types/tail.d.ts b/types/tail.d.ts index f15ae37..c7777a5 100644 --- a/types/tail.d.ts +++ b/types/tail.d.ts @@ -1,10 +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 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; diff --git a/types/util/tools.d.ts b/types/util/tools.d.ts index 2dd4618..d73166e 100644 --- a/types/util/tools.d.ts +++ b/types/util/tools.d.ts @@ -508,3 +508,15 @@ 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[]];