tc39 / proposal-type-annotations

ECMAScript proposal for type syntax that is erased - Stage 1
https://tc39.es/proposal-type-annotations/
4.26k stars 47 forks source link

What worng with Initial-value to tell the type of the varible #107

Open perymimon opened 2 years ago

perymimon commented 2 years ago

As a JS Developer for many years, I do not see any benefit to saying let i: number against let i = 1. The first way not telling me anything about the nature of the variable number or it's default value (Is it should be big, small, fraction ) . If it strings, what is the pattern of the string ( let name = "{first}" against let name: string )

Initial-value gives me a hint about the expectation of how to use this variable. also, it's native already.

Also the : grammar already in use for destructuring syntax

const {_foo: foo}  = someObject

The ONLY way I see a reason for typing is to give hints about variables that declare as object or array structure (if it is not already an instance of a class ).

Also, I think the whole idea of typing into the code is Super verbose and interferes with reading the code. if you like to add hints about typing comments above is much more conceivable and designing a pattern that does not clatter the reading and understanding of the code.

Maybe something like that for object

type person extend user{
  string first = "foo"
  string last  = "bar"
  number age = "18"
  function setAge ( number age )
  ...
}
// later

let vipPesron = type person

Now I know the keys of vipPerson and the default values of them. Also, because JS is a Dynamic typing language a pattern like that not misleading that the type of vipPerson is not dynamic anymore

benjamingr commented 2 years ago

I am not sure what this is arguing for/against. JavaScript would still be dynamically typed as a language (with pluggable type checking on top with these changes).

This doesn't mean const i = 1 would suddenly become invalid and even type checkers would infer it's a number in this case most likely.

So the sort of type system you are suggesting here would be enabled by this proposal rather than prohibited by it (anything after the keyword type is a comment)

theScottyJam commented 2 years ago
  1. Sometimes you don't want to provide defaults. e.g. not all function parameters need a default value
  2. Sometimes the type of a variable can't be inferred by how you're assigning it. e.g. let x: number | string = 2, or, let x: unknown = 2, etc. Sometimes you need to provide a couple of extra hints to the type-checker about your intentions
  3. What about support for generics? Interfaces? Often you need to provide extra type annotations when using, for example, a Map data structure, because it's simply unable to infer what its type should be by looking at how you instantiate the map.
  4. What about more complex types. I might be assigning the value { x: 2 } to a variable, but I need that variable to have the type { x: number, y: number }.
  5. Probably other reasons I'm not thinking of right now.
  6. UPDATE: TypeScript already supports inferring types based on the value you assign to the variable (allowing you to omit the type declaration in these scenarios), and yet, that's sometimes not good enough, hence the reason why TypeScript provides all sorts of other syntax constructs.

Have you ever actually used TypeScript before? If not, I'd invite you to try and use it on a smaller project as an exercise. You'll quickly find out that you do in fact need to provide type annotations to a number of places where you're not using defaults. In fact, the most important place I find myself using type annotations is on function signatures (most other logic gets inferred automatically), and often I'm not using defaults to help out the type-checker there.

simonbuchan commented 2 years ago

In general, the question is quite reasonable, though: why can't you depend on type inference?

There are two really core reasons, so far as I can tell:

It's possible that the second issue could be improved over time, but the first is something you need to deal with in documentation and/or guards in regular JavaScript anyway, having a more principled system for declaring exactly what you're promising to support can make both your and your users jobs easier.

perymimon commented 2 years ago

Thanks, everybody.

@benjamingr

"I am not sure what this is arguing for/against. JavaScript would still be dynamically typed as a language (with pluggable type checking on top with these changes)."

Check out this issue it explain it better then me: proposal-types-as-comments 62

@theScottyJam

"Sometimes you don't want to provide defaults"

  1. Can you bring an example when bad to bring a default? even the simpler one like '' (black string) or 0 (zero) Because in parallel I can say : Sometimes you don't want to provide type. "Sometimes the type of a variable can't be inferred by how you're assigning it"
  2. Are you mean the variable can be two possible types, like number or string? ok, I wrote an example below, after thinking about it a bit "What about support for generics"
  3. For Map, Set, Array, and genric objects we can use the type with extend keyword and just add the types annotation. there is an example below, but you can propose something else "What about more complex types."
  4. The example I provide covers that
  5. 🤷‍♂️when you think of them update us "TypeScript already supports inferring types based on the value you assign to the variable"
  6. I know. because that I do not understand why the syntax of the language need to be extended without reason

@simonbuchan Thanks

"Future compatibility"

  1. Changing the init value is the same complex as changing the type. but much less confusing. and saving the signature of a function is fundamental behavior in any design pattern. but maybe I do not fully understand you "It is a lot harder to understand type errors that have only inferred information"
  2. Maybe some annotation can solve that

So further to what I wrote above, I suggest the following update

type apple-hash extend Map{   // types extending native class, maybe snake case can be annotated that it types when we use it?
  set( string,  type apple )  // types for internal method. no need to implement anything 

}

type complex-type extend object {  
 number x = 4 // default values
 number y = 5 // default values
 number flexing 
 string  flexing // overload types
 pair-numbers flexing  // pair-numbers is custom type but no need to write `type` before in that case 
}

type pair-numbers extend object{  // object can be a default. so it can save typing in such cases
 number u, number v
}

// Because there is a type in the function it works as a root typed 
function ( num = 0, person = type person, apples = type apple-hash ) {
  //...implention...
}
theScottyJam commented 2 years ago

@perymimon

  1. Sometimes you don't want to provide defaults. e.g. not all function parameters need a default value

Can you bring an example when bad to bring a default? even the simpler one like '' (black string) or 0 (zero)

I did briefly mention that function parameters is one example. Let me provide a more concrete example of that:

function getUser(id) {
  if (id == null) throw new Error('An id is required')
  ...
}

This function allows you to get a user from a database via their id. The id is a required parameter. This means, there's no sensible default I could give it. If I made it have a default of 0, then that would make the parameter optional, and if you choose to call it without any arguments, it would look up a user with an id of 0, which is just silly. I could, instead, set it's default to be undefined or null, but then I'm not providing any helpful type information to the static type checker. How's the type checker supposed to know that the id is supposed to be a number?

Because in parallel I can say : Sometimes you don't want to provide type.

That's fine, this proposal doesn't force you to provide types. They're optional. Unless you're using a type-engine that forces you to use them everywhere (or you configured a type-engine to do that).

TypeScript already supports inferring types based on the value you assign to the variable

I know. because that I do not understand why the syntax of the language need to be extended without reason

Because automatic inferring isn't good enough. TypeScript provides this feature, and yet, people are still explicitly providing type information all over the place, because automatic inferring can only get you so far. It's limited. Even in languages which provide much stronger support for it, it's still generally recommended to at least specify the types of your functions (it's arguments and return type), because it helps you keep your API stable

So further to what I wrote above, I suggest the following update

I'm not sure if I understand how this works. Let's take this example function:

function doAction(action = type my-action-type, asUser = type my-user-type) { ... }

So tell me, if I call doAction() without any arguments, what will happen? By virtue of the fact that we're using default-parameter syntax there, I would hope that both action and asUser would be assigned to some sort of default value, but what would that be? undefined? What if I want it to be something else? What if, I want the action parameter to be a required parameter, and I want asUser to default to a system user.

Wouldn't it make more sense to just let default parameters be default parameters, and type annotations be type annotations, instead of trying to stuff two separate concepts into one syntax space? e.g. I could achieve what I wanted with type-annotations as follows:

function doAction(action: MyActionType, asUser: MyUserType = systemUser) {
  if (action == null) throw new Error('The action parameter is required')
  ...
}
simonbuchan commented 2 years ago

In short, @perymimon, we already have and can use type checkers without annotations. But if you think that's enough, you should probably try actually using Flow without annotations. (Flow tries a lot harder to infer everything than Typescript: I suppose you could try Hegel instead?)

Basically: it kinda works. But it's really really really annoying because you have no clue why one type isn't getting inferred and another is, and it's basically impossible to reach "fully typed" meaning you're not getting any actual confidence from the type system and have to continually double check it's work. Just using inference, even the already available far more powerful inference than just using the initial value is a huge step backwards.

The type system isn't psychic. It can't tell what you mean to do in the future. It can't tell what you meant code to handle, instead of what it does. It can't tell, when it manages to figure out that what you did doesn't make sense, what part is a problem. Annotations are solutions for all these problems.

abudulwadoodu commented 2 years ago

What about auto generating annotations based on inference ?

I mean, auto generate annotation If it is not already specified for a variable. This may prevent inferring data types on each executions and there by improve performance.

Auto generated annotation can be manually modified. This way it will be more accurate as well as auto code generation will save time for existing projects.

simonbuchan commented 2 years ago

Performance of what, the type-checker?

That actually should make things (very slightly) worse for Typescript at least, because now it doesn't just have to figure out the type of what's (for example) being assigned, but also parse and lookup the annotation in the compilation type registry, then verify that the right hand type is equivalent or compatible with the annotation type.

It's a bit more plausible for globally inferring checkers like Flow, I suppose?

theScottyJam commented 2 years ago

This may prevent inferring data types on each executions and there by improve performance.

From what I understand, the inference algorithms that are generally used are pretty fast, so this wouldn't really speed much up.

(Also, remember that type-checking has zero effect on execution performance. The type-checking happens before execution)

I mean, auto generate annotation If it is not already specified for a variable.

I would dislike this. The main place I provide types is on function parameters (where they're most important to have). I leave types off from most declarations, as they're simply not needed. Having a code-mod go through and add type information to every single declaration would be very noisy, and make the code difficult to read. And, if a human doesn't go through to make the types more concise (e.g. by providing human-readable names for larger type definitions), then you could get some extremely large type declarations appearing everywhere.

Plus, it sounds like you're talking about doing this sort of thing on a codebase that doesn't have any type annotations already provided. In TypeScript's case, that would mean that it would simply label almost everything with the any type (the any type basically tells TypeScript to not do type-checking against that variable).

This way it will be more accurate

I don't know about that.

Take this trivial example:

const fn = (a, b) => a + b

What is the types of those parameter? Is it the number type? Perhaps the BigInt type? Or strings? Or maybe it's meant to accept numbers, BigInts, and strings? Or, maybe, the first parameter is supposed to always be a string, and the second parameter can be whatever you want, and this function relies on JavaScript's automatic-string coersion to determine what the final value should be.

So, what's the answer? You can't tell without more context. TypeScript can't tell either. If you put this into TypeScript, it'll just take the safe route and say that the type's of a and b are any, i.e. don't type-check this function. It'll also say that the return type of this function is also any. For this means, if you run such a codemod on a codebase without any type information, almost all types will be auto-determined as any. So, if by more "accurate" you mean "have type-checkers mark everything as an 'any' type, and not type-check anything, just to be on the safe side and not incorrectly flag anything", then yes, you're correct. But, the type-checker will also be pretty useless. This sort of function requires humans to come in and flag the variables as a specific type to show intent.


Think about it this way. Say I have code that looks like this:

function getName(user) {
  return user.name
}

// elsewhere...
function doThing() {
  getName(undefined)
}

You can look at this and know that it's going to throw an error when getName() is called. So, should a type-checker flag this? Well, the problem is that this could all be working as expected. What if, the caller of doThing() has a try-catch, and makes default behavior happen if an error gets thrown? What if, this error is actually intentional, expected behavior that's intended to happen? Or, what if doThing() is actually dead code.

Because of this, when you first migrate to using Flow or TypeScript, they're generally very, very conservative in how they handle your existing code. It wouldn't be fun to install a type-checker, and have 1000 type errors, right off the bat, on your piece of software that has zero bugs. In TypeScript, all of this stuff would be littered with any types, and the type-checker will effectively ignore this code. It's not until a human comes in and starts adding type-related restrictions that TypeScript actually start type-checking that code. i.e. it's not until you say that getName() expects an object to be passed in that has a name property, that TypeScript will start throwing an error when doThing() tries to call getName() with undefined.

To sum this point up: TypeScript enables you to set restrictions on what is and isn't valid JavaScript, and until you lay those restrictions down (by hand), all of JavaScript is assumed to be valid. It's the code author's job to tell TypeScript what can and can't be passed into a function, and TypeScript will make sure the rules you tell it get followed.