Skip to content

Commit

Permalink
new(isNotEmpty) add types for isNotEmpty function, and update isEmpty (
Browse files Browse the repository at this point in the history
…#101)

Manually tested against `DefinitelyTyped`, all pass
  • Loading branch information
Harris-Miller authored May 2, 2024
1 parent ce48091 commit 5cea52a
Show file tree
Hide file tree
Showing 13 changed files with 277 additions and 57 deletions.
44 changes: 22 additions & 22 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
61 changes: 49 additions & 12 deletions test/head.test.ts
Original file line number Diff line number Diff line change
@@ -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<string | undefined>(head(''));
// string always return string
expectType<string>(head('abc'));
// emptyString still returns type string. this is due to ramda's implementation `''.chartAt(0) => ''`
expectType<string>(head(''));
expectType<string | undefined>(head('abc'));

// array literals will read the type of the first entry
expectType<string>(head(['fi', 1, 'fum']));
// but if the array is typed as an `Array<T> or T[]`, then return type will be `T`
expectType<string | number | undefined>(head(['fi', 1, 'fum'] as Array<string | number>));
// empty array literals return never
expectType<never>(head([]));
// but if it is typed, it will be `T | undefined`
// empty array literals return undefined
expectType<undefined>(head([]));

// typed empty array will be `T | undefined`
expectType<number | undefined>(head([] as number[]));
// const tuples return the literal type of the first entry
// as will a typed populated array
expectType<number | undefined>(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<number>(head([10, 'ten'] as [number, string]));
expectType<string>(head(['10', 10] as [string, number]));
// typed tuples return the type of the last element
expectType<boolean>(head([true] as [boolean]));
expectType<number>(head([10, 'ten', true] as [number, string, boolean]));
expectType<string>(head(['10', 10, false] as [string, number, boolean]));
// typed empty tuple returns undefined, this is expected because there is no `T` here
expectType<undefined>(head([] as []));

// typed arrays return `T | undefined`
expectType<number | string | undefined>(head([10, 'ten'] as Array<number | string>));
expectType<string | number | undefined>(head(['10', 10] as Array<string | number>));
expectType<number | string | undefined>(head([10, 'ten'] as ReadonlyArray<number | string>));
expectType<string | number | undefined>(head(['10', 10] as ReadonlyArray<string | number>));

// cross function testing with isNotEmpty
// test the type narrowing
const readonlyArr: readonly number[] = [];
if (isNotEmpty(readonlyArr)) {
expectType<number>(head(readonlyArr));
}

const readonlyArr2: readonly number[] = [];
if (!isNotEmpty(readonlyArr2)) {
// no-op
} else {
expectType<number>(head(readonlyArr2));
}


const arr: number[] = [];
if (isNotEmpty(arr)) {
expectType<number>(head(arr));
}

const arr2: number[] = [];
if (!isNotEmpty(arr2)) {
// no-op
} else {
expectType<number>(head(arr2));
}
36 changes: 36 additions & 0 deletions test/init.test.ts
Original file line number Diff line number Diff line change
@@ -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<string>(init('abc'));
// emptyString still returns type string. this is due to `''.chartAt(0) => ''`
expectType<string>(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<T> or T[]`, then return type will be `T`
expectType<Array<string | number>>(init(['fi', 1, 'fum'] as Array<string | number>));
// empty array literals return never
expectType<never[]>(init([]));
// but if it is typed, it will be `number[]`
expectType<number[]>(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<Array<number | string>>(init([10, 'ten'] as Array<number | string>));
expectType<Array<string | number>>(init(['10', 10] as Array<string | number>));

// works correctly with isNotEmpty
const arr = [1, 2, 3, 4];

expectType<number[]>(init(arr));

if (isNotEmpty(arr)) {
expectType<number[]>(init(arr));
}
45 changes: 45 additions & 0 deletions test/isNotEmpty.test.ts
Original file line number Diff line number Diff line change
@@ -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<ReadonlyNonEmptyArray<number>>(readonlyArr);
}

const readonlyArr2: readonly number[] = [];
if (!isNotEmpty(readonlyArr2)) {
// no-op
} else {
expectType<ReadonlyNonEmptyArray<number>>(readonlyArr2);
}


const arr: number[] = [];
if (isNotEmpty(arr)) {
expectType<NonEmptyArray<number>>(arr);
}

const arr2: number[] = [];
if (!isNotEmpty(arr2)) {
// no-op
} else {
expectType<NonEmptyArray<number>>(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<readonly [1, 2, 3]>(tuple2);
}
62 changes: 62 additions & 0 deletions test/last.test.ts
Original file line number Diff line number Diff line change
@@ -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<string | undefined>(last(''));
// string always return string
expectType<string | undefined>(last('abc'));

// array literals will read the type of the first entry
expectType<string>(last(['fi', 1, 'fum']));
// empty array literals return undefined
expectType<undefined>(last([]));

// typed empty array will be `T | undefined`
expectType<number | undefined>(last([] as number[]));
// as will a typed populated array
expectType<number | undefined>(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<boolean>(last([true] as [boolean]));
expectType<string>(last([true, 10, 'ten'] as [boolean, number, string]));
expectType<number>(last([false, '10', 10] as [boolean, string, number]));
// typed empty tuple returns undefined, this is expected because there is no `T` here
expectType<undefined>(last([] as []));

// typed arrays return `T | undefined`
expectType<number | string | undefined>(last([10, 'ten'] as Array<number | string>));
expectType<string | number | undefined>(last(['10', 10] as Array<string | number>));
expectType<number | string | undefined>(last([10, 'ten'] as ReadonlyArray<number | string>));
expectType<string | number | undefined>(last(['10', 10] as ReadonlyArray<string | number>));

// cross function testing with isNotEmpty
// test the type narrowing
const readonlyArr: readonly number[] = [];
if (isNotEmpty(readonlyArr)) {
expectType<number>(last(readonlyArr));
}

const readonlyArr2: readonly number[] = [];
if (!isNotEmpty(readonlyArr2)) {
// no-op
} else {
expectType<number>(last(readonlyArr2));
}


const arr: number[] = [];
if (isNotEmpty(arr)) {
expectType<number>(last(arr));
}

const arr2: number[] = [];
if (!isNotEmpty(arr2)) {
// no-op
} else {
expectType<number>(last(arr2));
}
16 changes: 14 additions & 2 deletions test/tail.test.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,14 +14,23 @@ expectType<[number, string]>(tail(['fi', 1, 'fum']));
// but if the array is typed as an `Array<T> or T[]`, then return type will be `T`
expectType<Array<string | number>>(tail(['fi', 1, 'fum'] as Array<string | number>));
// empty array literals return never
expectType<never>(tail([]));
expectType<never[]>(tail([]));
// but if it is typed, it will be `number[]`
expectType<number[]>(tail([] as number[]));
// single entry tuples return never, since they literally have no tail
expectType<never>(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<Array<number | string>>(tail([10, 'ten'] as Array<number | string>));
expectType<Array<string | number>>(tail(['10', 10] as Array<string | number>));

// works correctly with isNotEmpty
const arr = [1, 2, 3, 4];

expectType<number[]>(tail(arr));

if (isNotEmpty(arr)) {
expectType<number[]>(tail(arr));
}
16 changes: 8 additions & 8 deletions types/head.d.ts
Original file line number Diff line number Diff line change
@@ -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<T1, TRest>(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<T>(list: readonly [T, ...any[]]): T;
// non-empty arrays - Readonly here catches regular arrays too
export function head<T>(list: ReadonlyNonEmptyArray<T>): T;
// arrays, because these could be empty, they return `T | undefined
export function head<T>(list: readonly T[]): T | undefined;
11 changes: 10 additions & 1 deletion types/init.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
import { ReadonlyNonEmptyArray, NonEmptyArray } from '../es';

// string
export function init(list: string): string;
export function init<T>(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<T extends readonly [...any]>(list: T):
T extends readonly [...infer Init, any] ? Init :
T extends ReadonlyNonEmptyArray<infer A> ? A[] :
T extends NonEmptyArray<infer A> ? A[] : T;

Loading

0 comments on commit 5cea52a

Please sign in to comment.