Skip to content

Unable to assign Symbol.toStringTag to a class inheriting from Uint8Array #50923

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

Closed
matthewp opened this issue Sep 23, 2022 · 19 comments
Closed
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@matthewp
Copy link

Bug Report

πŸ”Ž Search Terms

symbol.toStringTag, uint8array

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about extending builtins

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

class Bytes extends Uint8Array {
  get [Symbol.toStringTag]() {
    return 'Bytes';
  }
}

πŸ™ Actual behavior

Property '[Symbol.toStringTag]' in type 'Bytes' is not assignable to the same property in base type 'Uint8Array'.
  Type 'string' is not assignable to type '"Uint8Array"'

As far as I'm aware you can add Symbol.toStringTag to any object.

πŸ™‚ Expected behavior

No error

@MartinJohns
Copy link
Contributor

MartinJohns commented Sep 23, 2022

Uint8Array already has the Symbol.toStringTag defined to return "UInt8Array". You can't declare a class that is a Uint8Array, but then returns a different type.

This is what you're trying to do:

class Base { get val(): 'base' { return 'base' } }
class Derived extends Base { get val() { return 'derived' } }

@matthewp
Copy link
Author

Then Uint8Arrays definitions are wrong, no? You can do this and it works in JS. It seems this objects definitions are overly strict / incorrect is the problem.

@MartinJohns
Copy link
Contributor

MartinJohns commented Sep 23, 2022

It's a consequence of TypeScripts structural typing. Without this specific return type, the different typed arrays would be structurally compatible, and allow you to assign them to one another. See #48617.

I don't know your code base, but I'd generally favor composition over inheritance.

@matthewp
Copy link
Author

@MartinJohns That issue shows that they are structurally the same. It looks like this has changed since then, as the example in that issue no longer succeeds.

As that issue is marked as Working as Intended then it seems that there is a regression here. Typed arrays are in fact, structurally identical and should be represented as such.

@MartinJohns
Copy link
Contributor

It shows that they are structurally the same, unless you import lib.es2015.symbol.wellknown.d.ts, which is what provides the toStringTag symbol (causing your issue).

@matthewp
Copy link
Author

Oh I see, then this is not a regression but an existing bug. Nevertheless the problem seems to be that the definitions for these types are incorrect.

@MartinJohns
Copy link
Contributor

It's neither a regression, nor a bug. It's intentional, to prevent these types from being structurally compatible. Without this explicit return type (provided in the lib "ES2015.Symbol.WellKnown"), you could assign a Float64Array to a Uint8Array (and vice versa). This is generally undesirable.

@matthewp
Copy link
Author

I don't agree, correct types that reflect reality are more important than working around limitations of the type system. Since these types are structurally identical they should be treated as such. The way to fix this problem is to add features; such as a compile-time only branding mechanism so you can make the objects structurally different without having that reflect on run-time code. Regardless, the type definitions should reflect what these properties actually return.

@fatcerberus
Copy link

You can’t please everyone: #31311

In both cases the types don’t reflect reality in some respect, but TS has to make a decision and the options here are mutually exclusive.

@fatcerberus
Copy link

The way to fix this problem is to add features; such as a compile-time only branding mechanism so you can make the objects structurally different

See #202 and, more recently, #21625.

@matthewp
Copy link
Author

Yeah to be clear I'm not proposing a feature in this issue. This issue would not be fixed by such a feature. This feature is fixed by updating the types to return a string, as that's how it is specced.

@MartinJohns
Copy link
Contributor

MartinJohns commented Sep 24, 2022

If they update the return type as proposed, they can also open #48617 again (which is working as intended and would break by your suggestion). Hence why fatcerberus said you can't please everyone.

Being 100 % spec compliant is not a goal of TypeScript, as per their language Design Goals. Instead they aim for a compromise between soundness and usability. It's unfortunate that TypeScript does not support nominal types for cases like this, but that's how it is for now.

@matthewp
Copy link
Author

matthewp commented Sep 24, 2022

To be clear, this is a bug and #48617 is a feature request. Normally you wouldn't create a bug in order to work-around the lack of a feature. That's why it's not clear to me that this bug is intentional, as you are suggesting.

In any event, I wonder if this can be fixed by adding a type-param? That way the normal non-extended case would continue to work as desired, but if you needed to extend you could provide a type param for the toStringTag return. Something like:

class Bytes extends Uint8Array<'Bytes'> {
  get [Symbol.toStringTag]() {
    return 'Bytes';
  }
}

Or perhaps another interface to keep the regular Uint8Array use-case cleaner?

class Bytes extends ExtendedableTypedArray<Uint8Array, 'Bytes'> {
  get [Symbol.toStringTag]() {
    return 'Bytes';
  }
}

@fatcerberus
Copy link

fatcerberus commented Sep 24, 2022

Normally you wouldn't create a bug in order to work-around the lack of a feature.

That actually happens often with TS due to various design constraints and is why the Not a Defect label exists:

"This behavior is one of several equally-correct options"

Note "equally-correct" doesn't imply the options are 100% correct, just that they have about the same "correctness value". Both options available today are wrong in some way:

  1. Without the literal-typed toStringTag (i.e. if it's typed as string) then typed array types are freely assignable to each other (and worse, ArrayBuffer), which doesn't reflect runtime reality.
  2. With it, you can't make subclasses return a different string (i.e. this issue), which doesn't reflect runtime reality.

I would go so far as to argue that point 2 is a minor inconvenience in most cases (you can always add a @ts-ignore, e.g., and most people don't bother to override this property in the first place) while point 1 leads to entire classes of bugs and therefore is the better option to mitigate in a statically typed language. All type systems will reject some correct programs; this is mathematically unavoidable.

@matthewp
Copy link
Author

ts-ignore doesn't carry over into declaration files. Curious your thoughts on the type param idea.

@fatcerberus
Copy link

I'm not a TS team member, but I doubt very much they'd want to add a type parameter and make the types generic just to specify the value of toStringTag. My guess is this issue gets closed as Design Limitation or Working as Intended, but you'll probably have to wait until Monday for an official response.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Sep 26, 2022
@RyanCavanaugh
Copy link
Member

This is the intended behavior; subclassing is subtyping and a class that returns a different value for [Symbol.toStringTag] is indeed not a subtype.

I would recommend writing

class Bytes extends Uint8Array {
  get [Symbol.toStringTag]() {
    return 'Bytes' as any;
  }
}

if you intend to make a new class with a subtyping violation (no judgment πŸ˜…).

If you're trying to get the "Bytes" wired through the type system, there are ways of accomplishing that, but I won't go into it unless you're really interested since they're rather involved.

@matthewp
Copy link
Author

Thank you for your time.

@jedwards1211
Copy link

jedwards1211 commented Jul 18, 2024

The fact that the libs define [Symbol.toStringTag]: string for some builtins and [Symbol.toStringTag]: 'Uint8Array' for others makes it seem like [Symbol.toStringTag] is being pragmatically abused by the libdefs here.

Perhaps there should be a way to declare "phantom" properties that can be referenced by types, but any runtime code that tries to access such properties is a compile error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

5 participants