tc39 / proposal-type-annotations

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

type annotations in destructuring #83

Open ehaynes99 opened 2 years ago

ehaynes99 commented 2 years ago

I'm curious what others think about how types could be implemented for destructured params. I'll focus on object destructuring here, though most of the same applies to array destructuring. The ECMA syntax was remarkably elegant due to its simplicity:

// ordered args
const test = (first, second) => null

// "named parameters"
const test2 = ({ first, second }) => null

It would have been really great if that simplicity had carried over to TypeScript:

const test = (first: string, second: number) => {}

// FICTIONAL TYPESCRIPT SYNTAX
const test2 = ({ first: string, second: number }) => {}

However, I feel it was a rather unfortunate choice in ECMA to use a colon for aliasing. It's perfectly functional in ECMA now. However, if I could go back in time, predicting the eventual adoption of type annotations, I would petition for the use of something else. I think as would have been a good choice simply because it would be consistent with aliasing in module imports, but really just about anything other than : would suffice:

// aliasing syntax
const test = ({ first: otherName, second }) => {}
// FICTIONAL ECMA SYNTAX
const test2 = ({ first as otherName, second }) => {}
// syntactically consistent with modules
import { thing as otherThing } from 'some-lib'

Because of the use of the colon, Typescript really had no choice but to completely disassociate the destructured values from the type, which IMO practically ruins the feature. I use it a lot less in TS than ECMA. It just feels "not worth it" unless the type being destructured is already defined elsewhere:

// ugh...
const test = ({ first, second }: { first: string, second: number }) => {}

// I use this one-off-args-type pattern for libraries, but it's exhausting...
type TestArgs = { first: string, second: number }

const test2 = ({ first, second }: TestArgs) => {}

Had ECMA used as for aliasing, types could have been mixed in:

// FICTIONAL TYPESCRIPT SYNTAX with aliasing
const test = ({ first as otherName: string, second: number }) => {}

Now it's a "can't unring the bell" situation, though, so worth looking for other options. If ECMA were to use a colon for type annotations, it would have the same issue that TypeScript has today. I think it would be a hard sell to use something else, though. It's at least a departure from a syntax familiar in quite a few other languages. Also, in 2015, I think it was fair for ECMA to ignore its impact on TypeScript, but today, I think it has enough muscle to discourage divergence. Still, for the sake of argument E.g. Haskell uses two:

const test = ({ first :: string, second :: number }) => {}

// but might be harder to read combined with single colon aliasing...
const test2 = ({ first: otherName :: string, second :: number }) => {}

Go simply puts a space between variables and their types, but that could be hard to read when combined with the other operators:

// not too bad...
const test = ({ first string, second number }) => {}

// but confusing with other operators allowed here
const test2 = ({ first: otherName string = 'foo', second number }) => {};

// maybe a little better in a different order, but still a bit hard to read
const test2 = ({ first string: otherName = 'foo', second number }) => {};

Perhaps some way to "decorate" the parameter types...

// probably ruins JSX
const test = ({ first: otherName <string>, second <number> }) => {}

// Java-ish cast
const test2 = ({ first: otherName (string), second (number) }) => {}

// something new
const test3 = ({ first: otherName %string, second %number }) => {}

I don't love the idea of prefix notation, but maybe a "bracket cast" could work:

// probably ruins JSX
const test = ({ <string>first, <number>second }) => {}

// Java-ish cast
const test2 = ({ (string) first, (number) second }) => {}
giltayar commented 2 years ago

I agree. This proposal might be the opportunity to fix this longstanding issue in existing TypeScript!

espoal commented 2 years ago

What about: const test = ({ first, second }: { string, number }) => {}

Totally agree with this. It's one of the biggest pain point for me when using typescript.

ehaynes99 commented 2 years ago

@espoal The issue with that is that the arguments again become positional. In general, property order of objects should be unknown to the compiler/runtime. It also gets confusing quickly with more complex types. E.g.

type User = {
  firstName: string;
  lastName: string;
  age: number;
  address: {
    streetAddress: string;
    city: string;
    state: string;
    postalCode: string;
  };
  contact: {
    type: string;
    value: string;
  }[];
};

becomes:

type User = {
  string;
  string;
  number;
  {
    string;
    string;
    string;
    string;
  };
  {
    string;
    string;
  }[];
};
spenserblack commented 2 years ago

Related TypeScript issue: https://github.com/microsoft/TypeScript/issues/29526

fleck commented 2 years ago

The :: for setting the type inside destructured params would be 💯

dfabulich commented 1 year ago

@matthew-dean You’ve posted a link to your proposal in a bunch of threads on this repository. I think the frequency at which you’re pushing your alternative is crossing the line into spam.

(I don’t think your proposal is a good one, because it isn’t as ergonomic as TypeScript or Flow. You don’t seem to value ergonomics as much as you value runtime introspection of types, which is a non-goal for me. Luckily, TypeScript is hugely popular, so I’m not too worried that your proposal will interfere with the work here.)

matthew-dean commented 1 year ago

@dfabulich My apologies. Removed.

Re: ergonomics - Of course it's not as ergonomic - TypeScript is a different language that's wholly incompatible with JavaScript. But pushing a dozen types of syntax "as comments" in JavaScript is not a sensible or ergonomic proposal either. The ergonomics of such a proposal should build on what currently is on the table in JavaScript (i.e. something like decorators).

My point was only that if you want to "decorate" params / vars, then it would make sense to extend the decorator syntax. Is it going to be as ergonomic as TypeScript? No, never. But that isn't the point of the OP proposal. If people want something as ergonomic as TypeScript, they can of course use TypeScript.

dfabulich commented 1 year ago

The point of the OP proposal is to unfork JavaScript.

The strong demand for ergonomic type annotation syntax has led to forks of JavaScript with custom syntax. This has introduced developer friction and means widely-used JavaScript forks have trouble coordinating with TC39 and must risk syntax conflicts. The OP proposal formalizes an ergonomic syntax space for comments, to integrate the needs of type-checked forks of JavaScript.

matthew-dean commented 1 year ago

@dfabulich You honestly consider carving out a bunch of TypeScript syntax and constructs as extra comment types within JavaScript as “ergonomic”? I mean, I get that it’s convenient for some (but not all) TS authors to some extent, but there has been no attempt at an alternative solution other than JSDoc, which is a strawperson argument for the proposal.

matthew-dean commented 1 year ago

I would consider it ergonomic to propose one (1) annotation construct for JS to use, such that there are 3 comment types, or 2 comment types and one annotation type, however you want to consider it. The actual syntax is not really the point.

dfabulich commented 1 year ago

"Alternate solution" to what?

I think you're looking for an alternative solution to "types in JavaScript" and there are quite a few of those; there's at least a dozen referenced right in the readme of this repository. Indeed, it's so easy to come up with new ones that you cooked one up in your spare time.

But none of those proposals will solve the problem of "unforking JavaScript," of allowing TypeScript and Flow users to stop transpiling, to eliminate the risk of future syntax conflicts. Unforking JavaScript (getting today's actual TS developers to stop transpiling) is a non-goal for you.

And, that's fine, I guess, if that's a non-goal for you, but the purpose of this proposal is to unfork JavaScript. An alternative approach to "types in JS" can't and won't do that job, unless it's so nice to use, so ergonomic, and, yes, so similar to TS and Flow that those developers would say "I'm switching to the new thing and skipping the transpilation step."

It's not enough to be an excellent implementation of types in JS. It has to cure the fork, too. This proposal is, by far, the best option currently on the table to do that.