-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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. |
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)
} |
Given the conversation about I was surprised that array methods, most notably type Characters = 'Abuela' | 'Julieta' | 'Agustín' | 'Mirabel' | 'Isabella' | 'Louisa';
const sisters = [
'Louisa',
'Isabella',
'Mirabel',
] satisfies Characters[];
const listCharacters = (characters: Characters[]) => characters.forEach((character) => console.log(character));
// satisfies FTW!
listCharacters(sisters);
// character cannot be passed into includes because the param is typed as 'Lousia' | 'Isabella' | 'Mirabel'
const isSister = (character: Characters):character is (typeof sisters)[number] => sisters.includes(character); It seems reasonable to expand the argument of |
(For convenience @dsongman Are you saying that the I don't think there's any plausible mechanism for that - ultimately the methods of arrays are defined by normal type system definitions. In this case: interface Array<T> {
includes(searchElement: T, fromIndex?: number): boolean
} In this case |
Yeah, that's right. Backing up to make sure I'm stating the problem and not necessarily the solution… I find that in the application codebases I've worked in, a very frequent issue that comes up is wanting to define a list of values via a union and then iterating over those values in the UI somewhere. If you're okay with duplication, you can make a type that ensures an array contains all values of a union. If you want a single source of truth, though, the general consensus seems to be to define an array of things, add If, however, you want to define an iteratable subset of a union, there isn't a great solution. Using
But while you can pass the subtype to functions that accept |
@dsongman Yeah, like I said, I just don't think there's a plausible mechanism for making that happen - it seems like a good use of
The difference between the
On the other hand, // The resulting type of `constSisters2` is identical to `satisfactorySisters`
const constSisters2 = [
'Louisa' as const,
'Isabella' as const,
'Mirabel' as const,
]; And you get an error with The point is that there isn't any lingering 'metadata' where the type system 'remembers' the (I recommend using the It's not impossible that |
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: