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

lib Fix Part 2/6 – Array-related methods #50450

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

graphemecluster
Copy link
Contributor

@graphemecluster graphemecluster commented Aug 25, 2022

General Information

PR separated out from #49855, because there might be some members expecting smaller PRs. As mentioned in the comments from the big PR, whether to review a single, large PR or 6 smaller PRs is up to the TypeScript Team to decide. I couldn't have found a better way for this; hopefully this will not bring any trouble to the Team.

This PR partially fixes #49773.
For details and the track list about the changes, please see #49773.

Part 2/6, Array-related methods

This is still a large PR, but I found no way to further split it because all the changes are related.

Fix #44191
Fix #45198
Fix #45199

@typescript-bot typescript-bot added the For Backlog Bug PRs that fix a backlog bug label Aug 25, 2022
@sandersn sandersn requested review from sandersn and weswigham and removed request for sandersn September 13, 2022 23:33
@sandersn sandersn self-assigned this Sep 13, 2022
@graphemecluster
Copy link
Contributor Author

Actually this is not as complicated as you think (there are actually no breaking changes). The large amount of changes is really only due to %TypedArray%. Please also test this PR with TypeScript bot @sandersn.

valueOf(): Uint8ClampedArray;

[index: number]: number;
}

interface Uint8ClampedArrayConstructor {
readonly prototype: Uint8ClampedArray;
new(length: number): Uint8ClampedArray;
new(length?: number): Uint8ClampedArray;
Copy link
Contributor

@yume-chan yume-chan Sep 27, 2022

Choose a reason for hiding this comment

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

I'm confused about these TypedArray types.

They are introduced in ES6 (ES2015), why are they in es5.d.ts? (Can we stop maintaining these old spec versions and merge them all)

This constructor without parameter is now declared in es2017.typedarrays.d.ts, but in es6 spec chapter 22.2.1.1 %TypedArray% ( ) they are already allowed.

@sandersn
Copy link
Member

@typescript-bot run dt
@typescript-bot test top100
@typescript-bot user test this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Sep 29, 2022

Heya @sandersn, I've started to run the diff-based user code test suite on this PR at 4bcd472. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Sep 29, 2022

Heya @sandersn, I've started to run the parallelized Definitely Typed test suite on this PR at 4bcd472. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Sep 29, 2022

Heya @sandersn, I've started to run the diff-based top-repos suite on this PR at 4bcd472. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

@sandersn Here are the results of running the user test suite comparing main and refs/pull/50450/merge:

Everything looks good!

@typescript-bot
Copy link
Collaborator

@sandersn Here are the results of running the top-repos suite comparing main and refs/pull/50450/merge:

Something interesting changed - please have a look.

Details

excalidraw/excalidraw

2 of 5 projects failed to build with the old tsc and were ignored

tsconfig-types.json

tsconfig.json

react-hook-form/react-hook-form

2 of 3 projects failed to build with the old tsc and were ignored

tsconfig.json

  • error TS2322: Type '{ type: string; message: string; }' is not assignable to type 'Merge<FieldError, (Merge<FieldError, FieldErrorsImpl<{ action: string; }>> | undefined)[]>'.
  • error TS2322: Type '(data: { checkbox: string[]; }) => { errors: { checkbox?: { type: "error"; message: string; } | undefined; }; values: {}; }' is not assignable to type 'Resolver<{ checkbox: string[]; }, any>'.

@graphemecluster graphemecluster changed the title lib Types and Documentations Fix, Part 2/6 lib Fix Part 2/6 – Array-related methods Oct 9, 2022

/**
* Returns a new array from a set of elements.
* @param items A set of elements to include in the new array object.
*/
of<T>(...items: T[]): T[];
of<T extends any[]>(...items: T): T;
Copy link
Member

Choose a reason for hiding this comment

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

possible break: better typing of tuples, meaning more precise downstream errors.

* @param end If not specified, length of the this object is used as its default value.
*/
copyWithin(target: number, start: number, end?: number): this;
copyWithin(target: number, start: number, end?: number): T[];
Copy link
Member

Choose a reason for hiding this comment

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

possible break: subtypes of Array will now just be Array, missing any of the subtype methods.

Is the only reason for this change consistency?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This has to be long.

Returning this in {Array, %TypedArray%}.prototype.{fill, copyWithin, sort} seems to be correct, but TypeScript's type system is purely type-of-value-based, not instance-based. And because a subtype such as a tuple, or the generic parameters of a type, can contain the information of the values, returning this, which means an object with a same type, is technically incorrect for these methods which mutate the original object in a disruptive way. For example:

declare const foo: [42, 1, 2, 3];
const bar = foo.fill(42); // !!?

interface ArraySubtype extends Array<string> {
	0: "foo";
}
const bar: ArraySubtype = ["foo", "bar", "baz"];
const bar1 = bar.sort(); // !!!

interface Int8ArraySubtype extends Int8Array {
	0: 42;
}
const baz = new Int8Array([42, 1, 2, 3]) as Int8ArraySubtype;
const baz1 = baz.sort(); // ?!!

In fact, I'd done the some thing on RegExp (it's a deprecated method though):

compile(pattern: string, flags?: string): RegExp;

I understand these are all rare cases, but since they return the same instance, people who find the original type desirable could just use the original thing. It just makes sense, and just because these are all rare cases it has to be corrected.


In contrast, the following is better typed this. That's why the this keyword in type level is misleading:

class Foo<T> {
	constructor(public foo: T) {}
	clone(): this {
		return Object.create(Object.getPrototypeOf(this), Object.getOwnPropertyDescriptors(this));
	}
}
class Bar<T> extends Foo<T> {
	bar = true;
}
new Bar(42).clone(); // still a Bar<number>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A use case of the above:

interface Array<T> {
	slice(): this;
}

(But then for ReadonlyArray, should the same method overload be typed this & T[]?)

(idea from #36554 (comment))

Copy link
Member

Choose a reason for hiding this comment

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

Is there a bug for this? Like the flatMap change in es2019, this isn't worth fixing unless it's breaking somebody's actual code, because any fix will break other code. Backward compatibility like this is the reason, for example, that we haven't been able to make overriding methods inherit parameter types.

* order, until it finds one where predicate returns true. If such an element is found, find
* immediately returns that element value. Otherwise, find returns undefined.
* @param thisArg If provided, it will be used as the this value for each invocation of
* predicate. If it is not provided, undefined is used instead.
Copy link
Member

Choose a reason for hiding this comment

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

from the repl, this === global when not provided. I'd either leave off the last sentence, or reference the rules for this.

* @param predicate find calls predicate once for each element of the array, in ascending
* order, until it finds one where predicate returns true. If such an element is found, find
* immediately returns that element value. Otherwise, find returns undefined.
* @param thisArg If provided, it will be used as the this value for each invocation of
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* @param thisArg If provided, it will be used as the this value for each invocation of
* @param thisArg If provided, thisArg will be used as the this value for each invocation of

* Returns the value of the first element in the array where predicate is true, and undefined
* otherwise.
* @param predicate find calls predicate once for each element of the array, in ascending
* order, until it finds one where predicate returns true. If such an element is found, find
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* order, until it finds one where predicate returns true. If such an element is found, find
* order, until predicate returns true for some element. If such an element is found, find

I would personally drop the second sentence and change the last one to start "If no element is found,". The second sentence is redundant with the first, though, and redundancy can be useful for understanding, so it's fine if you want to keep it.

@@ -182,7 +182,7 @@ interface BigInt64Array {
* @param thisArg An object to which the this keyword can refer in the predicate function.
* If thisArg is omitted, undefined is used as the this value.
*/
filter(predicate: (value: bigint, index: number, array: BigInt64Array) => any, thisArg?: any): BigInt64Array;
filter(predicate: (value: bigint, index: number, array: BigInt64Array) => unknown, thisArg?: any): BigInt64Array;
Copy link
Member

Choose a reason for hiding this comment

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

this probably won't break anything. But might!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess I would do it the other way round, i.e. changing all unknown to any then.

find<S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S | undefined;

/**
* Returns the value of the first element in the array where predicate is true, and undefined
Copy link
Member

Choose a reason for hiding this comment

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

looking at the change to unknown in es2020.bigint reminds me that technically 'true' here is technically 'truthy'. I don't know if it's worth calling out in the text, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, but this is another issue: whether there should be a syntax to allow returning anything in type predicates.

@@ -271,7 +271,7 @@ interface BigInt64Array {
* the accumulation. The first call to the callbackfn function provides this value as an argument
* instead of an array value.
*/
reduce(callbackfn: (previousValue: bigint, currentValue: bigint, currentIndex: number, array: BigInt64Array) => bigint): bigint;
reduce(callbackfn: (previousValue: bigint, currentValue: bigint, currentIndex: number, array: BigInt64Array) => bigint, initialValue?: bigint): bigint;
Copy link
Member

Choose a reason for hiding this comment

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

this change makes sense, but I don't understand why this overload exists in the first place.

/**
* Returns the primitive value of the array.
*/
valueOf(): T[];
Copy link
Member

Choose a reason for hiding this comment

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

possible (but unlikely) break since valueOf now has a more specific type

@@ -1245,8 +1249,7 @@ interface ReadonlyArray<T> {
* @param callbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array.
* @param initialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value.
*/
reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T): T;
reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T, initialValue: T): T;
reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T, initialValue?: T): T;
Copy link
Member

Choose a reason for hiding this comment

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

I can't imagine how this will be a breaking change, but removing/adding overloads nearly always is somehow

Copy link
Contributor Author

Choose a reason for hiding this comment

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

BTW is #7014 not resolved also due to compatibility? It has been an issue for years.

@sandersn
Copy link
Member

I marked what I think would be breaking changes. @andrewbranch Can you look at this for wording and possible breaks as well?

@RyanCavanaugh you might have opinions too.

@sandersn sandersn requested review from andrewbranch and removed request for weswigham October 11, 2022 23:53
@graphemecluster
Copy link
Contributor Author

To what extent are breaking changes acceptable? I agree that reverting some of these changes reduces the risk of causing compatibility problems, but to address the issues, I still think some of them are unavoidable. The recent changes to RegExpMatchArray is a good example. An appropriate extent of breaking changes shall benefit TypeScript users the most (this could possibly be modelled by something like $\mathit{benefit}\left(x\right) = e^{-x^2}$, maybe lol, just a joke).

Anyway, I would like to wait until all reviewers reach a consensus on what must be reverted and what are acceptable before I commit changes to it.

@graphemecluster
Copy link
Contributor Author

I notice that #47351 is an issue with Bug and Help Wanted tags (in contrast to those with Suggestion), and there exists non-breaking solutions while #36554 (comment) and #26976 (comment) seem to be the best. I guess you'd prefer a separate PR, but I'm happy to address it here.

@sandersn
Copy link
Member

Definitely a separate PR. Smaller changes to the standard lib are better.

@typescript-bot typescript-bot added the For Milestone Bug PRs that fix a bug with a specific milestone label Oct 31, 2022
@sandersn
Copy link
Member

sandersn commented Dec 17, 2022

To what extent are breaking changes acceptable?

I would say changes should target an improvement that feels like a "missing feature", or a fix for a specific bugs. Small improvements, like improved consistency or fixes for problems that nobody has noticed, are not worth it.

However, @RyanCavanaugh I'm sure has additional opinions and nuance.

Edit: Specifically, for this PR, fixing the three bugs in the description to add new methods is worthwhile, but needs to be done in a way that minimises changes elsewhere.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Backlog Bug PRs that fix a backlog bug For Milestone Bug PRs that fix a bug with a specific milestone
Projects
Status: Waiting on reviewers
4 participants