Modifying the Iterator Signature

My goal is to simplify handling two-dimensional arrays by creating a wrapper on the Array object. Although the code works, I encountered an issue with TypeScript complaining about the iterator signature not matching what Arrays should have.

The desired functionality I'm aiming for is to be able to loop through arrays as shown below:

const m = new Matrix([
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
]);

for (let { c, r, value } = m) {
   console.log(`Column: %s, Row: %s, Value: %s`, c, r, value);
}

To achieve this, I wrote the following code:

class Matrix<T=number> extends Array<Array<T>> {

    // Other methods...

    *[Symbol.iterator]() {
        for (let r = 0; r < this.length; r++) {
            for (let c = 0; c < this[r].length; c++) {
                yield { c, r, value: this[r][c] as T };
            }
        }
    }
}

However, TypeScript raises an error stating that

Type '() => IterableIterator<[number, number, T]>' is not assignable to type '() => IterableIterator<T[]>'.
Live Demo

How can I modify the code to appease TypeScript without resorting to using "Any" types and losing the benefits of strong typing?

Answer №1

Attempting to fulfill your request will undoubtedly pose a significant challenge, as it contradicts the essence of the type system. The fundamental concept known as the Liskov Substitution Principle dictates that when A extends B, an instance of A should be usable wherever an instance of B is expected. In simpler terms, every instance of A is inherently an instance of B.

By asserting that

Matrix<T> extends Array<Array<T>>
, you are essentially stating that a Matrix<T> is an Array<Array<T>>. However, if one were to iterate over an Array<Array<T>> using a for...of loop, the expectation would be to traverse through elements of type Array<T>. Such behavior is part of the prescribed interface contract of Array<Array<T>>. If, instead, elements of type [number, number, T] are encountered, there lies a discrepancy: a Matrix<T> does not qualify as an Array<Array<T>> according to the Liskov Substitution Principle.

The preferable approach to address this issue involves upholding the truth of

Matrix<T> extends Array<Array<T>></code by retaining the iterator method untouched and introducing an additional <code>unroll()
method to Matrix<T> for generating the desired iterator. This supplementary method preserves the substitution principle integrity since the absence of an unroll() method does not form part of the Array<Array<T>> contractual obligations.

An implementation may resemble the following:

class Matrix<T> extends Array<Array<T>> {
  constructor(data: T[][] = []) {
    super();

    // Populate matrix with provided data
    for (let r = 0; r < data.length; r++) {
      this[r] = [];
      for (let c = 0; c < data[r].length; c++) {
        this[r][c] = data[r][c];
      }
    }
  }

  *unroll(): IterableIterator<[number, number, T]> {
    for (let r = 0; r < this.length; r++) {
      for (let c = 0; c < this[r].length; c++) {
        yield [c, r, this[r][c]];
      }
    }  
  }

}

const m = new Matrix([[1, 2, 3], [4, 5, 6], [7, 8, 9]]);

for (let [c, r, value] of m.unroll()) {
  console.log(`Column: %s, Row: %s, Value: %s`, c, r, value);
}

If the inclination persists to override the iterator with a custom implementation, is such a feat achievable? 😅 Technically, yes. Given the restriction of adhering to the Array<Array<T>> constraints, crafting a novel methodology becomes imperative. Leveraging techniques like mapped and conditional types could delineate "an Array<Array<T>> sans a predefined iterator method," followed by asserting the suitability of the Array constructor for that particular construct and extending accordingly:

type _SortOfArray<T> = Pick<
  Array<Array<T>>,
  Exclude<keyof Array<any>, keyof IterableIterator<any>>
>;
interface SortOfArray<T> extends _SortOfArray<T> {}
interface SortOfArrayConstructor {
  new <T>(): SortOfArray<T>;
}
const SortOfArray = Array as SortOfArrayConstructor;

class Matrix<T> extends SortOfArray<T> {
  constructor(data: T[][] = []) {
    super();

    // Populate matrix with provided data
    for (let r = 0; r < data.length; r++) {
      this[r] = [];
      for (let c = 0; c < data[r].length; c++) {
        this[r][c] = data[r][c];
      }
    }
  }

  // Other auxiliary methods...
  *[Symbol.iterator](): IterableIterator<[number, number, T]> {
    for (let r = 0; r < this.length; r++) {
      for (let c = 0; c < this[r].length; c++) {
        yield [c, r, this[r][c]];
      }
    }
  }
}

const m = new Matrix([['1', '2', '3'], ['4', '5', '6'], ['7', '8', '9']]);

for (let [c, r, value] of m) {
  // c is a number, r is a number, value is a string
  console.log(`Column: %s, Row: %s, Value: %s`, c, r, value);
}

Everything seems to function as intended, right? Not exactly:

const filteredM = m.filter(row => row[0]!=='4'); 
// At compile time, filteredM is a string[][], but at runtime, it's Matrix<string>!

for (let hmm of filteredM) {
    // Compiler assumes hmm[0] is a string, however, it is actually a number
    console.log(hmm[0].toUpperCase()); // No compiler error but leads to runtime error!!
}

Due to the nature of how array extension operates, methods returning new arrays invariably return the extended version (referred to as the species of the array) by default. If a Matrix<T> is genuinely an

Array<Array<T>></code, this subspecies substitution ought to be seamless. Nonetheless, modifying it entails erroneous typings in all the methods of <code>Matrix<T></code that generate new arrays.</p>

<p>To rectify this discrepancy, outlining the fresh contract manually becomes necessary:</p>

<pre><code>interface SortOfArray<T> {
    [n: number]: Array<T>;
    length: number;
    toString(): string;
    toLocaleString(): string;
    pop(): T[] | undefined;
    push(...items: T[][]): number;
    concat(...items: ConcatArray<T[]>[]): SortOfArray<T>;
    concat(...items: (T[] | ConcatArray<T[]>)[]): SortOfArray<T>;
    join(separator?: string): string;
    reverse(): SortOfArray<T>;
    shift(): T[] | undefined;
    slice(start?: number, end?: number): SortOfArray<T>[];
    sort(compareFn?: (a: T[], b: T[]) => number): this;
    splice(start: number, deleteCount?: number): SortOfArray<T>;
    splice(start: number, deleteCount: number, ...items: T[]): SortOfArray<T>;
    // And so forth...

This meticulous process feels burdensome, especially considering the innate complications and questionable benefits associated. Additionally, numerous scenarios anticipating an

Array<Array<T>></code might trigger compiler errors upon receiving a <code>Matrix<T>
.

In conclusion, while opting to specify only relevant methods and properties remains a viable course, the arduousness of the task coupled with potential consequential issues renders it a less than appealing endeavor.


Wishing you the best of luck navigating this intricate terrain!

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

When using Typescript inheritance, the datatypes shown in IntelliSense are unexpectedly listed as "any" instead of

In my Typescript code, I have a small implementation where a class is either implementing an interface or extending another class. interface ITest { run(id: number): void } abstract class Test implements ITest { abstract run(id); } class TestEx ...

What is the method for accessing an anonymous function within a JavaScript Object?

Currently facing an issue with a Node.js package called Telegraf, which is a bot framework. The problem arises when trying to create typings for it in TypeScript. The package exports the following: module.exports = Object.assign(Telegraf, { Composer, ...

What could be causing routerLink to malfunction despite correct configuration?

Is routerLink properly placed in the view? <p><a routerLink="/registration" class="nav-link">Register</a></p> Checking my app.module import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular ...

What is the best way to configure the default entry point for a package.json file in a React

I'm having trouble with the default export in my package.json file. when I try to import: import { Component } from 'packagename/'; // size 22kb or import { Component } from 'packagename/dist' // size 22kb; but import { Component ...

Ways to display all current users on a single page within an application

I have come across a project requirement where I need to display the number of active users on each page. I am considering various approaches but unsure of the best practice in these scenarios. Here are a few options I am considering: 1. Using SignalR 2. ...

Tips on incorporating toggle css classes on an element with a click event?

When working with Angular typescript instead of $scope, I am having trouble finding examples that don't involve $scope or JQuery. My goal is to create a clickable ellipsis that, when clicked, removes the overflow and text-overflow properties of a spec ...

Experimenting with NGXS selectors: A comprehensive guide

Hey there, I am currently utilizing the NGXS state management library in my application. I have a selector set up like this and everything seems to be functioning correctly. However, while testing the app, I encountered the following error message: "PrintI ...

Is there a way to customize the appearance of a MUI5 Tooltip using emotion?

I went through the information in this Stack Overflow post and experimented with the styled method. The code snippet I used is as follows: import * as React from 'react'; import { styled } from '@mui/material/styles'; import Tooltip, { ...

How to Utilize Knockout's BindingHandler to Integrate JQuery.Datatables Select Feature?

I've developed a custom KO bindingHandler (view it here) to assist in updating the DataTable. The documentation for JQuery.DataTable.Select regarding how to access data requires a handle. You can see the details here. var table = $('#myTable&a ...

Setting the data type for a React Stateless Functional Component (SFC) in TypeScript

By assigning a type of React.FC<PropsType> to a variable, it becomes recognized as a React Stateless Functional Component. Here's an example: //Interface declaration interface ButtonProps { color: string, text: string, disabled?: boolean ...

Why is my root page not dynamic in Next.js 13?

I am currently working on a website using Next.js version 13.0. After running the next build command, I noticed that all pages are functioning properly except for the root page. The issue is that it's being generated as a static page instead of dynami ...

"Encountering a problem with the debounceTime operator in rxjs and HTTP requests while using the keyup

I have been working on implementing server-side search in Angular 7. I managed to find some code for implementation, but unfortunately it is not functioning as expected. The issue I am encountering is that when searching for a string, the code sends mult ...

Is it possible to provide unrestricted support for an infinite number of parameters in the typing of the extend function from Lodash

I am utilizing the "extend" function from lodash to combine the objects in the arguments as follows: import { extend } from 'lodash'; const foo1 = { item: 1 }; const foo2 = { item: 1 }; const foo3 = { item: 1 }; const foo4 = { item: 1 }; const f ...

An error in Typescript is indicating that a semicolon is expected. The identifier 'EventNameString' is currently being used as a value, even though it only refers to a type

I've been working on integrating Firebase phone authentication into an older Ionic project and have followed several tutorials. I was able to successfully implement it, but whenever I run ionic serve -l, I encounter the following error: Interestingly ...

The mat-table component in my HTML code is not displaying the dataSource as expected, even though I meticulously copied it from Material Angular

Although I am aware that my question may seem unusual, my issue precisely matches what the title conveys. The problem lies in my mat-table dataSource not displaying any data, even after attempting to log the data with console.log("My Data : ", this.dataSou ...

The seamless pairing of Cucumber and Playwright: Cucumber's inability to retain cookies results in a login attempt with every scenario

I am currently facing an issue with integrating cucumber and playwright into my framework. When attempting to execute various features or multiple scenarios within one feature, I encounter a problem where if one scenario logs into a site, the other scenari ...

How to declare a variable using new String() and s = '' in Typescript/Javascript

What is the correct way to declare an array of characters or a string in JavaScript? Is there a distinction between an array of characters and a string? let operators = new String(); or let operators = ''; ...

Looking for giphy link within a v-for loop (Vue.js)

I am fetching a list of movie characters from my backend using axios and rendering them in Bootstrap cards. My objective is to search for the character's name on Giphy and use the obtained URL as the image source for each card. However, when I attemp ...

Extending parent context in dependencies through OOP/Typescript as an alternative to using "extends"

Introducing a custom class called EventBus has been a game-changer for me. This class allows for easy attachment of on/off/once methods to any class that extends it, enabling the creation of an array of events that can be listened to. Currently, I find my ...

Ways to conceal an element in Angular based on the truth of one of two conditions

Is there a way to hide an element in Angular if a specific condition is true? I attempted using *ngIf="productID == category.Lane || productID == category.Val", but it did not work as expected. <label>ProductID</label> <ng-select ...