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. |
|
Also , we need Infinity literal type. Here is an example we use: type Element<T> = {
isArray: ArrayElem<T extends ReadonlyArray<infer E> ? E : never>;
notArray: T;
}[T extends ReadonlyArray<any> ? 'isArray' : 'notArray'];
type ArrayElem<T> = T extends ReadonlyArray<any> ? Element<T> : T;
declare function flatInfinity<T>(x: T): Array<ArrayElem<T>>;
const c = flatInfinity(['foo', 1, 2, null, [5, [2], [3, {}, 4]]] as const); // ("foo" | 1 | 2 | 5 | 3 | 4 | {} | null)[]it will be better if we can use |
|
type Vec3D = [number, number, number];
let vec: Vec3D = [1, 2, 3];
let scaledVec: Vec3D = vec.map(x => 2 * x); // error: Type 'number[]' is missing the following properties from type '[number, number, number]': 0, 1, 2The following overload using mapped tuples can be defined as workaround (as seen in #29841): declare interface Array<T> {
map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): { [K in keyof this]: U };
} |
declare interface Array<T> {
filter(filter: BooleanConstructor): Exclude<T, null | undefined | 0 | "" | false>[]
} |
|
In addition to @HoldYourWaffle's interface Box<T> { value: T }
type BoxTuple<T> = { [K in keyof T]: Box<T[K]> }
const forBoxes = <T extends any[], TValue> (...boxes: BoxTuple<T>) => ({
useValues: (callback: (v: T) => TValue) => {
// Argument of type 'any[]' is not assignable to parameter of type 'T'.
// 'any[]' is assignable to the constraint of type 'T',
// but 'T' could be instantiated with a different subtype of constraint 'any[]'.
return callback(boxes.map((box) => box.value))
// (Asserting the map call 'as T' fixes the error)
}
})
const values = forBoxes(
{ value: 'Dog' },
{ value: 1 },
{ value: true }
).useValues(
([str, num, bool]) => `My boxes contain ${str}, ${num}, and ${bool}`
) |
|
I would like concat.apply in _getEvents to work without the typecast of [] to ReadonlyArray.
|
|
I created #39259 . It appears that 2 of the signatures for reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T, initialValue: T): T;
reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: readonly T[]) => U, initialValue: U): U;It looks like the first can be dropped in favor of the second, as signature of the second covers the first. Here is a playground link demonstrating the issue. |
|
[@RyanCavanaugh]: @jimador and I were looking at #39259 together. Here is an even shorter repro path (playground link): function broken(): boolean {
return ['foobar', ''].reduce(
(previousValue, currentValue) => !!(previousValue && currentValue),
true
)
}
function works(): boolean {
return ['foobar', ''].reduce<boolean>(
(previousValue, currentValue) => !!(previousValue && currentValue),
true
)
}
function alsoWorks(): boolean {
const value = ['foobar', ''].reduce(
(previousValue, currentValue) => !!(previousValue && currentValue),
true
)
return value
}Which results in the following errors in the Hopefully, this will help. Academically, I'm very interested to know what mechanism causes the first and third ( EDIT: My working theory is the overload mismatch. Like @jimador said, maybe the simple solution is to only have the inferred generic signature? |
This has piqued my interest as well. |
|
@julian-sf That's interesting, it looks like TS is downcasting (or just plain inferring) the return type as Update: // the return type here is correctly inferred as `boolean`
function works() {
return new Array('foo', 'bar').reduce(
(previousValue, currentValue) => !!(previousValue && currentValue),
true
)
}
function broken(): boolean {
return new Array('foo', 'bar').reduce(
(previousValue, currentValue) => !!(previousValue && currentValue),
true
)
} |
|
Perhaps I'm missing something here, I'm not super experienced with more complex types, but this case looks impossible with TS right now. I have a const array of strings. I want an object with those strings as keys.
Desired behavior is for TS to infer the return type instead of just using the initial value as the return type. |
|
The signature for |
Technically you can achieve it with this:
But it's honestly not REALLY correct that way. There is no guarantee the object is correctly formed...that's why we think it's related to an overloading issue. IE, the less-useful signature is getting applied. |
|
In Typescript 3.9.7.
|
|
I believe that |
|
Typescript version 4.0.5 This one is a little longer than other examples here (I'm sorry), but I haven't been able to recreate this with a simpler function. The behaviour is very interesting though. Type inference for my const groupBy = <K extends string, V>(func: (el: V) => K) => (
acc: Record<K, V[]>,
el: V
): Record<K, V[]> => {
const key = func(el);
return { ...acc, [key]: (acc[key] || []).concat(el) };
};
type A = { a: string; b: number };
const listA: A[] = [];
// 'el' is inferred to be of type 'A' as expected
const a = listA.reduce(groupBy(el => el.a), {});
type B = { a: string; b: string[] }; // Note that 'b' is a list
const listB: B[] = [];
// 'el' is inferred to be of type 'string | B'
const b = listB.reduce(groupBy(el => el.a), {}); // Type error: Property 'a' does not exist on type 'string | B'The I also found that manually deleting the following override from reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; |
|
With This would make code like this function better with the // currently head is `string | undefined`, but in practice, it's always `string`
const [head, ...rest] = someString.split("/");There are, AFAIK, two caveats:
Technically, those could be addressed with overloads, but at least the second one feels like a reasonable sacrifice of soundness for the sake of pragmatism. |

Formed in 2009, the Archive Team (not to be confused with the archive.org Archive-It Team) is a rogue archivist collective dedicated to saving copies of rapidly dying or deleted websites for the sake of history and digital heritage. The group is 100% composed of volunteers and interested parties, and has expanded into a large amount of related projects for saving online and digital history.




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: