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!