-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Array method definition revamp: Use case collection #36554
Comments
From #19535: const foo: [number, string][] = [[1, 'one']];
const a = foo.concat([2, 'two']);
// SHOULD ERROR,
// the actual content of 'a' is [[1, 'one'], 1, 'two']
// so a[1] should be number | string | [number, string]
a[1][0]; |
From #24579: declare let arr: [[number, boolean], string];
let x0 = arr.flat(0); // Should be [[number, boolean], string] or (number | boolean | string)[]
let x1 = arr.flat(1); // Should be [number, boolean, string] or (number | boolean | string)[]
let x2 = arr.flat(2); // Should be [number, boolean, string] or (number | boolean | string)[] |
From #26976: // Should be OK (currently an error) and produce string[]
let a1 = [].concat(['a']);
// Should be OK (is) and continue to produce string[]
let a2 = ['a'].concat(['b']);
// Should either error (current behavior) or maybe produce (string | number)[]
let a3 = [1].concat(['a']); |
From #29604: // Should be an error
const a: boolean[] = [[17], ["foo"]].flat();
// Should be an error (stretch goal)
const b: boolean[] = Array.prototype.concat([17], [19], [21]); |
This should error (previously misidentified as "should not error"): // Should error because add([["a"]], ["b"]) will not produce a string[][]
function add<A>(arr: Array<A>, el: A): Array<A> {
return arr.concat(el)
} |
From a real-world code suite broken by #33645: // Did not error; probably shouldn't in the future either
class A {
flattenTree(option: any, changeOnSelect: any, ancestor = []) {
let flattenOptions: any = [];
const path = ancestor.concat(option);
flattenOptions = flattenOptions.concat(this.flattenTree(option, changeOnSelect, path));
}
} This one should be simpler to demonstrate exactly what aspect of things got broken |
([] as any[]).reduce(() => 0, 0); // Expected: number, Actual: any
([] as unknown[]).reduce(() => 0, 0); // Expected: number, Actual: unknown
([] as never[]).reduce(() => 0, 0); // Expected: number, Actual: number |
@RyanCavanaugh This is caused by a mistake of the order of overloads. I made #36570 and looks like it makes no regression. |
@RyanCavanaugh Shouldn't it? If Should that implementation be updated -> |
@jablko good point. The actual code was in |
@RyanCavanaugh I think this should be an error? The reason it currently doesn't error is because I think the |
TypeScript Version: 3.8.x Code interface Bar {
property: boolean;
}
let foo: ReadonlyArray<string> | Bar;
if (Date.now() === 0) {
foo = { property: true};
} else {
foo = ['Hello'];
}
if (Array.isArray(foo)) {
console.log(foo[0]);
} else {
console.log(foo.property); // <-- error
} Expected behavior: Actual behavior: Playground Link: link |
@bpasero Old example
|
@HolgerJeromin I just did what @RyanCavanaugh suggested in #37129 (comment) and posted my usecase here. |
Copied example from #46438 (per @andrewbranch's request): // The type of result is `number`, but should be `string`.
const result = [1, 2, 3].reduce((acc, value) => {
// ~~~~~~~~~~~~~~~~~
// ^-- Error Here: "No overload matches this call"
if (acc === null) {
return '' + value;
} else {
return acc + ', ' + value;
}
}, null);
console.log(result); // "1, 2, 3"
console.log(typeof result); // "string" |
I'd like to be able to pass Playground Link (version 4.4.4) But the gist of it is: const bar = arrayOfFoosAndBars.find(isBarOrBaz); // does not work
// this behavior already works for if statements
if (isBarOrBaz(fooOrBarValue)) {
// correctly narrows to just the overlapping type, `Bar`
} The proposed change is to add: interface Array<T> {
find<S>(predicate: (this: void, value: S | T, index: number, obj: T[]) => value is S, thisArg?: any): Extract<T, S> | undefined;
} (And something similar for |
I think we could introduce changes to some of these methods in a backwards-compatible way. Here's a definition of declare interface Array<T> {
concat<U>(this: Array<U>, ...items: ConcatArray<U>[]): Array<U>;
concat<U>(this: Array<U>, ...items: (U | ConcatArray<U>)[]): Array<U>;
}
[1].concat('x') // error as expected
[1].concat<number | string>('x') // No error since type is explicitly widened Playground: https://tsplay.dev/wXkDJW I think this approach should work for most of the non-mutating Array methods in question. |
I don't know if this is appropriate here, but i have just been bitten by this. Imho, //strict compiler settings
const arr = [1, 2, 3];
const onlyPositives = arr.filter(num=>{
num > 0 //forgot return, TS doesn't mind
});
console.log("onlyPositives", onlyPositives); // [] empty array
//isPositive has type (num:number)=>void
const isPositive = (num:number)=>{
num > 0;
}
//filter should not accept a predicate not returning a boolean
const onlyPositives2 = arr.filter(isPositive);//no error that way either Instead, the predicate type is actually returning Sorry if this has already been discussed to death, but i find this very unsound and i havn't been able to find a duplicate. |
This would be a pretty big breaking change, as many people will use e.g. Perhaps, instead, the type could be modified in some way to allow |
@vjau You can also add the @typescript-eslint/no-unnecessary-condition lint rule - it tries to find dead conditionals and checks these array predicates. |
I was under the impression that it was idiomatic TS to write |
Hey @RyanCavanaugh, please could I get #41708 added to this so that #44216 can finally get merged/fixed. |
Array methods such as splice, map, etc. when called on a subclass of Array, return a new instance of the subclass. This might be related to tuples losing their tuple-ness when calling map, etc., but I'm not sure if that's the same issue. tuples exist only in the type system, while a subclass exists at runtime, so I could see this potentially being a separate issue from the tuple issue. |
@EthanRutherford be aware that browsers are exploring removing that capability, so it should not be relied upon. (also, "Tuple" can refer to the proposed JS language primitive when using a capital T) |
Ah, I was not aware of that. |
After looking further into https://github.com/tc39/proposal-rm-builtin-subclassing, it appears that support for "Type II: subclass instance creation in built-in methods" support is unlikely to be possible to remove without significant webcompat issues. One of the champions has even responded that the removal of type II is highly unlikely: tc39/proposal-rm-builtin-subclassing#21 (comment). In light of that, and the fact that the proposal is only stage 1, perhaps it would be prudent for Typescript to try to match the current behavior, rather than anticipate what seems to be a rather unlikely future behavior change? |
declare const test1: 1
declare const test2: 1 | 2
declare const test3: 2 | 3
declare const test4: number
declare const array: (0 | 1)[]
array.includes(test1) // OK
array.includes(test2) // Argument of type '1 | 2' is not assignable to parameter of type '0 | 1'. Type '2' is not assignable to type '0 | 1'.(2345)
array.includes(test3) // Argument of type '2 | 3' is not assignable to parameter of type '0 | 1'. Type '2' is not assignable to type '0 | 1'.(2345)
array.includes(test4) // Argument of type 'number' is not assignable to parameter of type '0 | 1'.(2345) In this example, the result of array.findIndex((v) => v === test1) // OK
array.findIndex((v) => v === test2) // OK
array.findIndex((v) => v === test3) // This condition will always return 'false' since the types '0 | 1' and '2 | 3' have no overlap.(2367)
array.findIndex((v) => v === test4) // OK Disputably, some people may even argue that even |
I hope this is not out of scope, but when one of the mentioned array methods is called on a tuple of length Compiler settings: default, but with const aTuple = [ "a", "b", "c"] as const
const bTuple = [ "x", "y", "z"] as const
// These are both correctly inferred as exactly 3
const aLength = aTuple.length
const bLength = bTuple.length
type range = 0 | 1 | 2
const possibleIndex = 2 as range // This could be something like getRandomNumber(0,2)
// This is inferred as x | y | z and doesn't include undefined, as expected
const possibleAccess = bTuple[possibleIndex]
// This causes an error, because the return array could contain undefined
const test: string[] = aTuple.map((_, i) => {
// With noUncheckedIndexedAccess=true access is inferred as possibily undefined,
// but TS could/should know that the the index will always be 0 | 1 | 2, as above
const access = bTuple[i]
return access
}) Here is the playground |
Expected const ab: number[] | string[] = ([] as string[] | number[]).filter(
(value) => false,
); The error is
Actual const ab: (string | number)[] = ([] as string[] | number[]).filter(
(value) => false,
); Typescript Version: 5.2.2 Related: #38380. |
I would appreciate if Array.includes would behave like this: interface Array<T> {
includes(searchElement: unknown, fromIndex?: number): searchElement is T;
}
interface ReadonlyArray<T> {
includes(searchElement: unknown, fromIndex?: number): searchElement is T;
} |
it depends, the most you can guarantee in general is const abcs: String[] = "abcs".split('')
abcs.includes("d") // > false, even though "d" matches type String |
@karlismelderis-mckinsey Yeah this has been suggested before e.g. #31018 and there's some issues. One is, like @icecream17 said it can return false even when But also, I think widening
The type-guard signature you suggest is useful for the second case, but makes the first case worse: if Personally, I suggest making a utility function and using it in place of function includes<const S>(haystack: readonly S[], needle: unknown): needle is S {
const _haystack: readonly unknown[] = haystack;
return _haystack.includes(needle)
} |
We've gotten numerous issue reports and PRs to change the methods of
Array
, particularlyreduce
,map
, andfilter
. The built-in test suite doesn't cover these very well, and these methods interact with each other and the surrounding contextual type in fairly subtle ways.@jablko has done a great job at #33645 collecting a variety of issues into a single PR; we need to augment this PR (or something like this) with a proper test suite so we can be sure about what's being changed.
I'd like to create a clearinghouse issue here to collect self-contained (I CANNOT POSSIBLY STRESS THIS ENOUGH, SELF-CONTAINED, DO NOT IMPORT FROM RXJS OR WHAT HAVE YOU) code samples that make use of the array methods.
Please include with your snippet:
Once we've established a critical mass of code snippets, we can start combining the existing PRs into an all-up revamp and assess its impact to real-world code suites to figure out which changes don't result in unacceptable breaking changes.
self-contained
The text was updated successfully, but these errors were encountered: