Closed theScottyJam closed 2 years ago
Are these types basically comments, that the language does not enforce at all, but anyone can build a type checker that's able to read and understand these comments?
No. These are real types that implementers can utilize to optimize data structures and callstacks. This is mentioned in the introduction. While the types make for nice self-documenting code and relatively simple documentation generation, the primary goal is to allow more native-like performance.
As for enforcement, I don't know how to write spec algorithms. I would need a TC39 champion to handle that.
Do these type annotations have a runtime impact, for example, if a function declares it accepts an object of a particular shape, will the runtime ensure that only objects of that shape get passed in?
Yes. If an object doesn't match a defined interface you will get a TypeError exception. This adds function performance implications when using types.
For example, are we able to change the browser-based APIs so they have type definitions, without breaking any untyped code that's relying on it?
Yes. If this were implemented I'd expect all native APIs could be redefined using types and overloads. These are basically identical to current documentation. In Visual Studio for instance when using document.createElement you will see a function signature that looks like createElement(tagName:any, options?:ElementCreationOptions)
. These kind of function signatures are more or less identical to ones you'd see later in typed libraries.
With function overloading it's possible that optional arguments would just be split off into separate functions many times, so they might look different. createElement could look like:
createElement(tagName:null) {}
createElement(tagName:string) {}
createElement(tagName:string, options:ElementCreationOptions) {}
Also as mentioned in the proposal, an example of ECMAScript methods that would change would be the static ones on the Math object. All of these would be specialized and optimized for each numeric type.
Ah, ok.
A handful of other questions then.
Say I have code like the following:
class User {
name: string
constructor(name) {
this.name = name
}
}
const user = new User()
What happens if I do user.name = 24
? Is an error immediately thrown? If so, it would mean I can't use types on existing classes (granted, I can't think of many built-in APIs that simply expose properties publicly like this). If not, when does the type-checking happen?
What about this snippet:
interface MyThing { ...lots of properties... }
function fn(thing: MyThing) { ... }
When I pass an object into fn()
, is JavaScript checking if the object conforms to the interface, by iterating over each property on the interface and ensuring it exists on the object? So, if I use a lot of smaller functions and pass this object around a lot, the same check will be happening a lot?
What happens if I do user.name = 24? Is an error immediately thrown?
Refer to this: https://github.com/sirisian/ecmascript-types/issues/29 I haven't thought about it in a while, but it's definitely a big question. Because of how dynamic objects are you can effectively change them completely making their "type" meaningless.
If we had a kind of "final" then yes, setting a property not defined in the class would throw. I've also considered that defining any public, private, or static member with a type would make it final and the class would need to be marked dynamic to allow members to be added.
This issue exists especially with memory mapped objects which I want to exist intuitively. Adding arbitrary members to an object means it no longer maps to the same static memory layout. In practice I'd expect every class implemented after types are introduced to use "final"/be non-dynamic wherever applicable.
When I pass an object into fn(), is JavaScript checking if the object conforms to the interface, by iterating over each property on the interface and ensuring it exists on the object? So, if I use a lot of smaller functions and pass this object around a lot, the same check will be happening a lot?
Depends on the implementation. A fully typed call stack can in theory skip some checks. I do think it will occur though especially with more dynamic objects as the shape must match.
interface MyThing { a:uint32, b:uint32 }
function f(thing: MyThing) {
}
function g(thing: MyThing) {
f(thing);
}
let o = { c: 10 };
g({ a: 1, ...o }); // TypeError
In that case during run-time a check is required for the first call to g. A smart parser can probably determine that such a structure would never pass the type check. In cases where you just do g({ a: 1, b: 2 }) with a constant object the parser could skip such checks as it knows it matches all the time. If you write code that deletes or changes the function it has to recompute such things as expected, but changing functions at run-time is rare.
You do make me wonder if changing a matched interface should be allowed. Specifically making something no longer match the interface. I think I'll add some examples defining this behavior.
interface A { a: uint32 }
function f(a:A) {
delete a.a; // TypeError: Property 'a' is required.
}
There's other complex scenarios.
interface A { a:uint32 }
interface B { a:string }
function f(a:A) { }
function f(b:B) { }
function g(a:A|B) {
f(a); // Checked again to find the right overload
}
g({ a: 'a' }); // Requires a check to see if it matches. Finds it matches interface B.
An engine can expand all code paths and generate the overloads for g automatically. This can get insane as the parameters add more possible expansions. (Could just generate them as they're needed though).
interface A { a:uint32 }
interface B { a:string }
function f(a:A) { }
function f(b:B) { }
function g(a:A) {
f(a); // No check needed now since f(a:A) is the only possible code-path
}
function g(a:B) {
f(a); // No check needed now since f(b:B) is the only possible code-path
}
g({ a: 'a' }); // Requires a check to see if it matches. Finds it matches interface B.
Alright, that makes sense.
You do make me wonder if changing a matched interface should be allowed. Specifically making something no longer match the interface.
I was actually trying to throw together a types-for-javascript repository, and have been contemplating these same issues. What I put together was mostly meant to be as a starting point for discussions, and wasn't really intended to become a full-fledge proposal. I was mostly focusing on the logistics of dealing with these sorts of issues (like, how to deal with someone mutating an object in a performant way), and didn't really spend time discussing the different features we could incorporate in a type system (like union types, etc). This is when I stumbled upon this repository - I see a lot of hard work has gone into this, trying to flesh out the specific details of all sorts of features, which is awesome! From my initial read through it, I couldn't figure out what the plans were for handling some of these difficult issues, which is why I opened up this thread - I think you've answered my main questions now though. If it interests you, you can take a peak at how I was trying to approach things, to see if it helps at all with this repository.
Added a section clarifying things.
The README goes into a ton of detail on the syntax of a potential type system, but it fails to explain how the types will actually get enforced. Are these types basically comments, that the language does not enforce at all, but anyone can build a type checker that's able to read and understand these comments? Will there be a new, standard type checker? Do these type annotations have a runtime impact, for example, if a function declares it accepts an object of a particular shape, will the runtime ensure that only objects of that shape get passed in?
Along a similar vein, is it possible to retroactively add types to existing APIs without breaking backward compatibility? For example, are we able to change the browser-based APIs so they have type definitions, without breaking any untyped code that's relying on it?