Skip to content

Class expressions: illegal assignments are allowed  #39469

Closed
@karol-majewski

Description

@karol-majewski

TypeScript Version: 4.0.0-dev.20200707

Search Terms: Subclassing, class expression, constructor, inheritance

Expected behavior:

My anonymous class expression should not be assignable to SortedArray because the brand is missing.

Actual behavior:

The class expression is assignable to SortedArray. This compiles, but breaks in runtime.

Related Issues:

Code

I wanted a nominal, array-like structure to represent sorted arrays. It should have the same API as ReadonlyArray. I'm subclassing the built-in Array constructor under the hood.

interface SortedArray<T> extends ReadonlyArray<T> {
  readonly brand: unique symbol; // This brand will be missing
}

interface SortedArrayConstructor {
  new <T>(items: T[], comparator: Comparator<T>): SortedArray<T>;
}

// This assignment should not be legal
const SortedArray: SortedArrayConstructor = class<T> extends Array<T> {
  constructor(items: T[], comparator: Comparator<T>) {
    super(...items.slice().sort(comparator));
  }
}

This breaks in runtime — we never defined brand on the anonymous class.

// Uncaught TypeError: Cannot read property 'toString' of undefined
new SortedArray([1, 3, 2], comparator).brand.toString()
Output
"use strict";
const SortedArray = class extends Array {
    constructor(items, comparator) {
        super(...items.slice().sort(comparator));
    }
};
var Comparison;
(function (Comparison) {
    Comparison[Comparison["LessThan"] = -1] = "LessThan";
    Comparison[Comparison["Equal"] = 0] = "Equal";
    Comparison[Comparison["GreaterThan"] = 1] = "GreaterThan";
})(Comparison || (Comparison = {}));
const comparator = (left, right) => {
    if (left === right) {
        return Comparison.Equal;
    }
    else if (left < right) {
        return Comparison.LessThan;
    }
    else {
        return Comparison.GreaterThan;
    }
};
// Uncaught TypeError: Cannot read property 'toString' of undefined
new SortedArray([1, 3, 2], comparator).brand.toString();
Compiler Options
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "useDefineForClassFields": false,
    "alwaysStrict": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "downlevelIteration": false,
    "noEmitHelpers": false,
    "noLib": false,
    "noStrictGenericChecks": false,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "esModuleInterop": true,
    "preserveConstEnums": false,
    "removeComments": false,
    "skipLibCheck": false,
    "checkJs": false,
    "allowJs": false,
    "declaration": true,
    "experimentalDecorators": false,
    "emitDecoratorMetadata": false,
    "target": "ES2017",
    "module": "ESNext"
  }
}

Playground Link: Provided

Metadata

Metadata

Assignees

Labels

BugA bug in TypeScriptEffort: ModerateRequires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual".Fix AvailableA PR has been opened for this issueHelp WantedYou can do this

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions