Right but a null object is null for a reason. It is not the same type as something that has a non-null prototype - nor is it the same as something that eventually inherits from null.
The important differentiator wrt inheritance is the direct [[Prototype]], and null is just as much a characteristic as “inherits from a specific object”.
Setting aside all the criticism against this idea, I really like it! A long time ago I introduced the same idea by the name - type preservation. You can search for it if you want.
I would like to alter the syntax & semantics of it a bit; hope you'll forgive me😅.
let x := "string"
x = 3 // error
let y := { a: 1, b: 2, c: 3 }
y = String // valid
y = { d: 4 } // valid
y = String("hi") // error
// leveraging Type Hints proposal
let a: string = "foo"
// same as - let a /* string */ := "foo"
// usage in functions
(x := 0) => x ** 2
function(x : boolean = true) { ... }
// lets add a little extra :)
// if used with a const the constant is considered immutable / frozen
const arr := [1, 2, 3, 4]
inspiration - Go
Here's how it works:
The := operator evaluates the rhs and preserves the primitive type for the lhs identifier. The primitive type may / may not be consistent with the typeof operator (we should discuss this a little more - how it should distinguish builtin objects like Array and handle null)
sidenote:
I believe clarity and developer productivity is more important than cutting a little bit on performance😉.
I get your reasoning, and as I said, the issue with null is something I consider negotiable as null is even mishandled in the language. It's supposed to be a primitive that can be assigned to an object without being an object itself, but somehow, the null primitive is an object. That's what causes this situation. Since it is an object, it can be replaced by any other object as it is of the type "object". At least, that's my understanding, and how I would currently choose to handle things, to keep everything consistent with how null currently works.
Besides, an object with a non-null prototype has null in it's prototype chain. If that weren't the case, then I would probably choose differently. Maybe it would have been better for undefined to terminate a prototype chain, or maybe something new like void. Who knows, but I'd need to see some reasonable rationality on why to disregard the null at the end of every prototype chain.
Yes, that was very similar to what I'm trying to achieve. However, I don't want anything but a new keword and associated semantics to be added to the language. Even this keyword takes advantage of how existing features in the language currently work, being that a type is associated with every variable already. This keyword just flags that the type of a particular variable is not allowed to be changed, while also allowing that type to encompass prototypes if the primitive type is "object"
As do I, which is why I prefer the keyword syntax (and would have preferred private over #). The keyword syntax makes it obvious that something different is happening, and makes it easier to apply consistently to all syntax domains without disrupting their flow. Take a look at what you had to do to get your approach to work in a function. That's not consistent with how you were doing it everywhere else. I also don't see how you'd apply that to object properties consistently.
I get what you're saying, but that wouldn't work. Consider an inheritance chain for a moment. If I have a class Farmer extends Person, and the variable is fixed like let fixed person = new Person, are you saying I couldn't assign a new Farmer as the person?
Because of the is a relationship. Remember, ES is a prototype-oriented language. Except for primitives, and the strangeness TC39 insisted on with class fields, the prototype of an object defines its type. Always has. To change that now would fundamentally alter the language.
You say “always has” but “its type” just isn’t a thing in the language, modulo typeof (which isn’t useful here) I hear that that’s the mental model you employ, but that isn’t an objective given.
Since it's been called to question again, I'll just flat out specify it here.
The objective of adding fixed to the language is as follows: Provide a means of constraining the type of a variable. Where the desired type is primitive, it should only allow values of that primitive type to be assigned. Where the desired type is an object, it should only allow values possessing the same prototype to be assigned.
Is there currently any feature in JavaScript where that is true?
Why would a subclass not satisfy the requirement? That is how subclasses usually work — you can substitute an instance of a subclass wherever an instance of the base class is required.
Can you define "the same prototype"? Do you mean, Object.is(Object.getPrototypeOf(original), Object.getPrototypeOf(replacement))? The spec can't rely on an object having the .constructor property, so I'm not sure what other definition you'd have available.
Explicitly, this is true. However, all of the intrinsic objects are implicitly typed. You can't create an object that behaves precisely like an Array, give it the Array prototype, and expect it to work. Sure, this is because of the internal slots that won't exist on the object. However, from the perspective of a developer, this is because the new object you created isn't of the same type as Array. Likewise, if you extend Array, you'll get a subclass that passes the Array.isArray() test. Again, we both know that this is because the instance object is the one created by the Array constructor and thus has the correct internal slots. However, from the perspective of a developer, it's because the new class is a subclass of Array and is naturally expected to be able to do everything an Array does unless the new class disables that feature somehow.
I get what you're trying to say. However, my point is that you're trying to tow a line that is neither consistent with nor compatible with the way the average developer uses the language or understands the corresponding concepts. Doing that not only makes the mental model harder for the average developer to absorb, but also makes the new feature less useful to the majority of users.
You're making an assertion about what "the average developer" thinks, that I don't think would be backed up by evidence if either of us claimed it. I don't think most developers think about internal slots, but I also don't think that the way you described it is the common mental model.
That's almost it. There's a few false implications in your code. While by "the same" I do indeed mean identity, that's not the key part of the sentence. The key part is "possessing", by which I mean the following.
let original = new SomeClass;
let replacement = new SomeOtherClass;
let oProto = Object.getPrototypeOf(original);
let same = oProto.isPrototypeOf(replacement);
We can agree to disagree on that. It's not fruitful to argue that point so I won't go there any further. My simple point is that there is a large contingent of developers, if not the majority, who would expect that polymorphism would be upheld for such a keyword.
@ljharb - I've got to agree with @rdking here. I don't know of any language, including typed variants of Javascript, that don't accept subclasses if you requested a superclass. e.g. This is valid typescript:
class Shape {}
class Square extends Shape {}
const myShape: Shape = new Square()
@rdking's suggestion here is supposed to be a form of type safety. If we're going to deviate from this normal and expected behavior, we would need a really good reason to do so, and this issue with "null" does not seem to constitute as a "good enough reason". We could even just ban "null" from being an allowed default type if we have to, if that were to make this issue go away.
fixed let myShape = new Square()
// ...
class Circle extends Shape {}
myShape = new Circle() // allowed?
I would expect not if Square is defining the fixed type when intention was really for it to be a Shape. This could limit the usefulness of fixed in the case of objects if there's no way to upcast from the initializer type.
Yeah, I would suspect your suspicions are true. You would have to do something like this:
class Shape {}
class Circle extends Shape {}
fixed let myShape = new Shape()
myShape = new Circle()
which means Shape can't be an abstract base class, it has to be something that's intended to be initialized. And you would want to have an instance of the base class be the initial value of this variable.
In general, there seems to be a number of limitations with this proposal, which is why I think a more direct type assertion system would work better, like this:
let myShape: Shape = new Circle()
myShape = new Square()
The way you wrote it, no. That wouldn't work. However, it can work with a slight adjustment.
//Assuming Square extends Shape
let fixed myShape = Square.prototype;
// ...
class Circle extends Shape {}
myShape = new Circle() // allowed
This works because Square's prototype is a Shape instance.
False. All that's required is that there is an object whose prototype is the desired prototype. Initialized or not is not relevant unless you intend to use the object. So even if Shape is abstract,
let fixed myShape = Object.create(Shape.prototype);
works just fine.
I presumed from senocular's question that he was trying to initialize the fixed variable with a subclass instance because the base class was not available for whatever reason at that point in the code. I would also like to see a more direct type declaration. However, that's a severe overhaul to the language. What I'm suggesting here has much less severe requirements to implement. It's actually just a partial restriction on all the automatic conversion that's usually being done. But it serves the purpose of declaring the type, albeit indirectly.