Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add array-map for keeping tuple length when using .map #136

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions package.json
Expand Up @@ -72,6 +72,11 @@
"types": "./dist/array-index-of.d.ts",
"import": "./dist/array-index-of.mjs",
"default": "./dist/array-index-of.js"
},
"./array-map": {
"types": "./dist/array-map.d.ts",
"import": "./dist/array-map.mjs",
"default": "./dist/array-map.js"
}
},
"keywords": [],
Expand Down
32 changes: 32 additions & 0 deletions readme.md
Expand Up @@ -7,6 +7,7 @@ TypeScript's built-in typings are not perfect. `ts-reset` makes them better.
- 🚨 `.json` (in `fetch`) and `JSON.parse` both return `any`
- 🤦 `.filter(Boolean)` doesn't behave how you expect
- 😡 `array.includes` often breaks on readonly arrays
- 😭 `array.map` on a tuple looses the tuple length
tjenkinson marked this conversation as resolved.
Show resolved Hide resolved

`ts-reset` smooths over these hard edges, just like a CSS reset does in the browser.

Expand Down Expand Up @@ -293,6 +294,37 @@ const validate = (input: unknown) => {
};
```

### Keeping the tuple length in resulting tuple from `Array.map`

```ts
import "@total-typescript/ts-reset/array-map";
```

When you're using `Array.map` with a tuple, the length is lost. This means you loose the guard against accessing an item out of bounds.
tjenkinson marked this conversation as resolved.
Show resolved Hide resolved

```ts
// BEFORE

const tuple = [1, 2, 3] as const;
const mapped = tuple.map((a) => a + 1);

// oops. There's no 4th element, but no error
console.log(tuple[3]);
```

With `array-map` enabled, this code will now error:

```ts
// AFTER
import "@total-typescript/ts-reset/array-map";

const tuple = [1, 2, 3] as const;
const mapped = tuple.map((a) => a + 1);

// Tuple type 'readonly [number, number, number]' of length '3' has no element at index '3'.
console.log(tuple[3]);
```

## Rules we won't add

### `Object.keys`/`Object.entries`
Expand Down
15 changes: 15 additions & 0 deletions src/entrypoints/array-map.d.ts
@@ -0,0 +1,15 @@
/// <reference path="utils.d.ts" />

interface ReadonlyArray<T> {
map<U>(
callbackfn: (value: T, index: number, array: readonly T[]) => U,
thisArg?: any,
): { [K in keyof this]: U };

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
): { [K in keyof this]: U };
): { readonly [K in keyof this]: U };

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey @ahrjarrett what difference does this make? Could we test it?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my mind, if this doesn't seem to make a difference, that only happens to be true for the set of inputs we've tried it with.

Fixing the output of a readonly array's map operation to be readonly seems like a reasonable thing to do. It creates a cohesive API, its semantics are unambiguous, and it eliminates the possibility that one of the property modifiers might change out from under us.

Probably a moot point anyway, since I don't know if this addition is in keeping with the design goals of ts-reset. That said, I'm personally all for preserving structure like this where possible, so I went ahead and made the suggestion :)

}

interface Array<T> {
map<U>(
callbackfn: (value: T, index: number, array: T[]) => U,
thisArg?: any,
): { [K in keyof this]: U };
}
1 change: 1 addition & 0 deletions src/entrypoints/recommended.d.ts
Expand Up @@ -6,3 +6,4 @@
/// <reference path="set-has.d.ts" />
/// <reference path="map-has.d.ts" />
/// <reference path="array-index-of.d.ts" />
/// <reference path="array-map.d.ts" />
77 changes: 77 additions & 0 deletions src/tests/array-map.ts
@@ -0,0 +1,77 @@
import { doNotExecute, Equal, Expect } from "./utils";

doNotExecute(async () => {
const tuple = [0, 1] as const;
const mapped = tuple.map(
(
value: (typeof tuple)[number],
index: number,
source: readonly (typeof tuple)[number][],
) => 1,
);

tuple.map(() => 1, {});

type tests = [
Expect<Equal<(typeof mapped)["length"], 2>>,
Expect<Equal<(typeof mapped)[0], number>>,
];

mapped[0];
mapped[1];
// @ts-expect-error
mapped[2];
});

doNotExecute(async () => {
const tuple = [0, 1] as [0, 1];
const mapped = tuple.map(
(
value: (typeof tuple)[number],
index: number,
source: (typeof tuple)[number][],
) => 1,
);

tuple.map(() => 1, {});

type tests = [
Expect<Equal<(typeof mapped)["length"], 2>>,
Expect<Equal<(typeof mapped)[0], number>>,
];

mapped[0];
mapped[1];
// @ts-expect-error
mapped[2];
});

doNotExecute(async () => {
const arr: readonly number[] = [0, 1];
const mapped = arr.map(
(
value: (typeof arr)[number],
index: number,
source: readonly (typeof arr)[number][],
) => 1,
);

arr.map(() => 1, {});

type tests = [Expect<Equal<(typeof mapped)["length"], number>>];
});

doNotExecute(async () => {
const arr: number[] = [0, 1];
const mapped = arr.map(
(
value: (typeof arr)[number],
index: number,
source: (typeof arr)[number][],
) => 1,
);

arr.map(() => 1, {});

type tests = [Expect<Equal<(typeof mapped)["length"], number>>];
});