Skip to content
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

Easier destructuring with type annotations on binding patterns #29526

Open
5 tasks done
Richiban opened this issue Jan 22, 2019 · 154 comments
Open
5 tasks done

Easier destructuring with type annotations on binding patterns #29526

Richiban opened this issue Jan 22, 2019 · 154 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@Richiban
Copy link

Search Terms

type inference destructuring syntax conflict

Suggestion

It's currently not possible to destructure a value in Typescript and provide types for each of the values due to the syntax clash with destructuring and renaming at the same time. You can see exactly this issue in the Typescript FAQs at: https://github.com/Microsoft/TypeScript/wiki/FAQ#why-cant-i-use-x-in-the-destructuring-function-f-x-number------

This is frustrating when programming in React where it's very common to see this pattern:

const MyComponent = ({ a, b }) => {
    // ...
}

But in Typescript a and b are untyped (inferred to have type any) and type annotation must be added (either to aid in type safety or to avoid compiler errors, depending on the state of the user's strict flags). To add the correct type annotation it feels natural to write:

const MyComponent = ({ a : string, b : number }) => {
    // ...
}

but that's not what the user thinks due to the aforementioned syntax clash. The only valid syntax in Typescript is actually this:

const MyComponent = ({ a, b } : { a : string, b : number }) => {
    // ...
}

Which is very strange to write and difficult to read when the object has more than two parameters or the parameters have longer names. Also the value names have been duplicated -- once in the destructuring and once in the type annotation.

I suggest we allow some other symbol (my current thinking is a double colon) to make the syntax unambiguous in this specific scenario:

const MyComponent = ({ a :: string, b :: number }) => {
	// ...
}

Although this is really the only place it would be used, for the sake of consistency, I think is should be allowed everywhere:

const a :: string = "";
const b :: number = 1;

Use Cases

It would allow for type-safe destructuring of values where the type cannot be inferred by the compiler (such as function parameters).

Examples

A good example of the sort of React components I'm talking about (and one of the first Google results for React Functional Components) can be found at https://hackernoon.com/react-stateless-functional-components-nine-wins-you-might-have-overlooked-997b0d933dbc. We can use my proposed syntax in the functional component definition:

import React from 'react'

const HelloWorld = ({name :: string}) => {
	const sayHi = (event) => {
		alert(`Hi ${name}`)
	}

	return (
		<div>
			<a href="#"
			   onclick={sayHi}>Say Hi</a>
		</div>
	)
}

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@j-oliveras
Copy link
Contributor

Duplicate/related to #7576, #29019

@dragomirtitian
Copy link
Contributor

dragomirtitian commented Jan 22, 2019

I know expression level syntax is a no-no, but how about:

const MyComponent = ({ ... } : { a : string, b : number }) => {
    // ...
}

With the ... meaning auto-spread all the rest of the properties in the object.

You can still have a traditional binding list if you want to remap any names:

const MyComponent = ({ a:aa, ... } : { a : string, b : number }) => {

}

There is a reason this issue keeps poping up. The current solution is painful.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Jan 23, 2019
@RyanCavanaugh
Copy link
Member

I agree it's a duplicate but it really does suck. We should try again.

@DanielRosenwasser DanielRosenwasser changed the title A new sigil for specifying type when deconstructing Destructuring with type annotations (take 2) Jan 27, 2019
@DanielRosenwasser DanielRosenwasser changed the title Destructuring with type annotations (take 2) Easier destructuring with type annotations on binding patterns Jan 27, 2019
@olmobrutall
Copy link

With React hooks just released, class components are being... slowly deprecated in favor of functional components all the way.

This feature is now more important than ever.

The :: syntax sounds good to me.

@scottmas
Copy link

scottmas commented Apr 30, 2019

There are indeed multiple duplicates of this request. Heavyweights of the JS ecosystem have said they want it. Random dudes such as myself want it. Unscientific polls show that a large majority of JS developers use argument object destructuring (https://twitter.com/rauschma/status/987471929585143809). It seems to me that the time has come for some solution to be made :)

FWIW, the double colon syntax seems good to me.

@TazmanianD
Copy link

Yeah, this seems like the 3rd iteration of this issue with the previous two just being closed as being too complicated to implement but I suspect that people are going to continue to ask for it and I'll add my voice to the list. This is something that's actually making it harder for me to convince my teammates to switch to TypeScript because this type of destructuring in function calls is pretty common in our existing code.

@gynet
Copy link

gynet commented Jul 31, 2019

The current syntax for this scenario does look bad... I wish ES6 could choose different syntax for renaming, instead of the colon, e.g 'as'; the 'as' keyword is more meaningful for renaming purpose :)

Anyway, although the proposal of double colon syntax does not look bad, it could ergonomically cause troubles for developers to understand what does it mean since people get used to using a single colon as the type annotation. I would prefer another way to address the problem. Actually, I like dragomirtitian's proposal better.

@Richiban
Copy link
Author

While I also think @dragomirtitian 's solution is a reasonable one, I'd like something more in keeping with Typescript's philosophy of not introducing any new syntax other than type annotations. one of the reasons for Typescript's success has been its mantra of "It's just Javascript, but with types". It's being able to look at a declaration and immediately parse out what's TS and what's JS:

//          The Javascript bit
//               --------
const myFunction({ a, b } : SomeType) { ... }
//                        ----------
//                     The Typescript bit

If TS starts introducing its own expression / destructuring syntax I fear that starts us down a slope of TS being its own language, rather than a type system for JS.

@dragomirtitian
Copy link
Contributor

Actually the more I think about :: the more I like it 😁, but with a slightly different meaning. Let's not consider :: as a new way to introduce a type annotation, but rather an empty rename.

// rename, no type annotations
const HelloWorld = ({name : newName }) => { 

}
// rename, with annotation 
const HelloWorld = ({name : newName : string }) => { 

}

// empty rename, with annotation 
const HelloWorld = ({name :: string }) => { 

}
// empty rename, with annotation, and default
const HelloWorld = ({name :: string = "default" }) => { 

}

I personally think it flows nicely. The first : always introduces a name binding, which can be empty, the second : always introduces a type annotation, and since a new : not valid here in ES it would not be ambiguous.

Would work decently with objects too:

type FooBar = { foo: string; bar: number }
const HelloWorld = ({ a, b:{ foo: fooLocal, bar: barLocal }: FooBar  }) => {
  
}

const HelloWorld = ({ a, b::FooBar  }) => {
  
}

const HelloWorld = ({ 
  a: number, 
  b: { 
    foo:fooLocal:string, 
    bar:barLocal:string
  }
}) => {
  
}

@Richiban
Copy link
Author

@dragomirtitian

Let's not consider :: as a new way to introduce a type annotation, but rather an empty rename.

Works for me! 👍

@gynet
Copy link

gynet commented Aug 2, 2019

@dragomirtitian Good point!

@danm
Copy link

danm commented Oct 30, 2019

Were angle brackets considered? #34748

@fr0
Copy link

fr0 commented Dec 9, 2019

Has any progress been made on this? The current solution is not good since it is not DRY, e.g. ({foo, bar, baz}: {foo: string, bar: number, baz: boolean}) has a lot of duplicated information.

@danm
Copy link

danm commented Dec 9, 2019 via email

@fr0
Copy link

fr0 commented Dec 9, 2019

@tienpv222
Copy link

Not really related, but it'd be much more easier if object destructuring alias uses as instead of :, just like how import does.

@dragomirtitian
Copy link
Contributor

@pynnl that ship sailed a long time ago unfortunately.

@VanCoding
Copy link

TypeScript really needs this! I can't think of a more annoying thing than this. Any syntax is okay, as long as it gets implemented. Please. This has been discussed for such a long time. There are a lot of options that don't collide with the ES spec.

jeremyschlatter added a commit to henryperson/bel-repl that referenced this issue Apr 22, 2020
My motivation for this was to resolve some linter warnings about this
code:

    switch (type) {
      case "input":
        return <div key={index}>> {text}</div>
      case "output":
        return <div key={index}>{text}</div>
    }

There were two warnings, which both amounted to 'but what if `type` is
neither "input" nor "output" ?'

I could have fixed these warnings by making type a bool using an if
statement or ternary operator here. But that seemed less clear than the
current code. If I had ADTs I could use them instead. JavaScript does
not have ADTs, but TypeScript kinda does!

There were a few extra changes I had to make to satisfy the typescript
compiler:

- Type inference didn't follow how we changed the `solarized` object
through `Object.assign`, so I used two different objects instead.

- Some numeric CSS properties are marked as numbers rather than strings
in the DefinitelyTyped repo. This includes `flexGrow` and `rows`, so I
changed those to numbers.

- TypeScript assumed the parameters to Space were both required, but
they should both be optional. There wasn't any way to add annotations
inline for those params (see microsoft/TypeScript#29526),
so I slightly changed how Space is written.

- TypeScript noticed that replInputField could be null. I'm not sure if
that could actually come up in practice, but I changed our usage s.t.
if it happens we won't get a null pointer dereference.

- TypeScript noticed that our useEffect call depends on the (mutable)
value of replInputField, so I added it as a data dependency.

- TypeScript noticed that there were a couple places where we were
inadvertently setting replState to undefined (via not setting a value
for that field in a setCombinedState call). I added reasonable values
for those cases.

- I also updated the aforementioned switch statement such that it no
longer causes a warning, which was the whole point of this exercise.
@mathieucaroff
Copy link

I'd like to propose a variant of the multi-colon syntax:

{ x:: number }
{ x:: number = 123 }
{ xin: xout:: number }
{ xin: xout:: number = 123 }

The idea is to keep the double colon, even when remapping the name, so that the type is always identified by the colon-block (or colon-square) operator ::. The point of this subtlety is to make it easy for the reader to visually parse where the name ends and where the type begins. This is achieved through the use of a different operators rather than being purely syntax-based.

@mathieucaroff
Copy link

Here is another idea of syntax, which resembles the "Auto spread" proposition of @dragomirtitian: {...}: { x: number }. It actually extends the syntax of "Interface spread to variable" proposed by @antanas-arvasevicius in #14856, adding support for renaming variables:

let ...{ x: number } = v // x is now available
let ...{ x: number = 123 } = v
let ...{ xin xout: number } = v // xout is now available
let ...{ xin xout: number = 123 } = v
let ...{ left { data dataLeft: string }, right { data dataRight: string } } = v
// dataLeft and dataRight are now avaliable

Example with a function, a let together with the rest parameter:

const formatSidedData = (...{ left { data a: string }, right { data b: string } }) => `${a}<->${b}`

let ...{ person { name: string, age: number }, ...metadata: Record<string, string> } = getInfo()

Note: the ellipsis ... "type annotated destructuring-declaration" can only be used at the top level, either in a const, let or var list of variable declarations, or among the arguments of a function. I suggest a standard destructuring-declaration should not be allowed to use ... inside it to create a zone of "type annotated destructuring-declaration".

One of the unique nice properties only found in @dragomirtitian proposal, that of @antanas-arvasevicius and this one is that in the simple case --without renaming--, the syntax used is just the usual syntax for interfaces. This covers the use case of easily transforming a type annotated destructuring into an interface (by unedited copy-paste). This also applies to copy-pasting an interface to a "type annotated destructuring".

Below is an example to clarify.

Let there be a function magnitude, computing the length of a vector. Using my "Extended interface spread to variable" syntax, we can write this:

function magnitude(...{ x: number, y: number }) {
  return (x ** 2 + y ** 2) ** 0.5;
}

We then realize we want to name the interface. It is easy to just copy-paste it to a named type:

interface Point { x: number, y: number }

function magnitude({ x, y }: Point) {
  return (x ** 2 + y ** 2) ** 0.5;
}

If you use Prettier, it will automatically reformat the interface to this:

interface Point {
  x: number;
  y: number;
}

Doing the opposite change (going back) is also easy, though you may need to remove some semicolons and add commas:

function magnitude(...{ x: number, y: number }) {
  return (x ** 2 + y ** 2) ** 0.5;
}

This syntax can be used without any issue with the traditional ellipses for varargs functions:

function join(...{ begin: string, sep: string, end: string }, ...parts: string[]) {
    return begin + parts.join(sep) + end;
}

@mathieucaroff
Copy link

Content

Vocabulary

  • Type annotation: telling Typescript what value the assignment should validate
  • Type assertion: telling Typescript to ignore the assigned value and use ours instead
  • Remapping (renaming): Operation of giving a local name different than the one in the map while destructuring the map.
  • Remapping (sub-destructuring): Operation of destructuring the nested value inside an object, making them available as local variables
  • Default value: Operation of providing a value to use in replacement of a missing or undefined property in the destructured object.
  • "Type annotated destructuring": Operation of destructuring and provide type annotations for the resulting local variables, without repeating the object property name, nor the local variable name
  • "Complete type annotated destructuring": Type annotated destructuring which supports remapping (both for renaming and sub-destructuring) and providing default values while destructuring.

All the issues I could find

Note:

  • The issue which produced the most work on the subject before June 2018 was Destructuring with type annotations #7576 "Destructuring with type annotations".
  • Corresponding issue in Flowtype: "Type annotations inside destructuring" #235

Reason(s) to decline the feature request (back in 2016)

@mhegazy

  • ultimately, every new syntax added is added complexity, for the user to learn and recognize, and for the compiler code base to maintain, so usually we try to limit complexity unless the value warrants it. and for destructuring, the syntax is already complex to start with, and we did not want to add yet another layer on top.

-- seconded by @DanielRosenwasser

Proposition(s) with unusable syntax

Here are a few propositions I found whose syntax cannot be used.

{ x: number }
{ xin: number = 123 }
{ xin as xout: number }
{ xin as xout: number = 123 }

The first two lines are valid JS and TS. This syntax is incompatible.

Formatting variants among valid syntaxes

(This is about formatting recommendations and auto-formatting)

  • double_colon.sparse1
{ x:: number }
{ xin:: number = 123 }
{ xin: xout: number }
{ xin: xout: number = 123 }
  • double_colon.sparse2
{ x :: number }
{ xin :: number = 123 }
{ xin: xout: number }
{ xin: xout: number = 123 }
  • double_colon.compact2 (formatting variant)
{ x::number }
{ xin::number = 123 }
{ xin: xout: number }
{ xin: xout: number = 123 }
  • double_colon.compact4 (formatting variant)
{ x::number }
{ xin::number = 123 }
{ xin:xout:number }
{ xin:xout:number = 123 }

Typescript 3.9.2 (current situation)

  • current_grouped
{ x }: { x: number }
{ xin = 123 }: { xin: number }
{ xin: xout }: { xin: number }
{ xin: xout = 123 }: { xin: number }
  • current_split_and_named
interface Prop { x: number }
{ x }: Prop

interface Prop { x: number }
{ xin = 123 }: Prop

interface Prop { xin: number }
{ xin: xout }: Prop

interface Prop { xin: number }
{ xin: xout = 123 }: Prop

All valid syntax propositions I could find

All these are proposals for "Complete type annotated destructuring".

(order attempts to group propositions sharing similar traits)

{ x as number }
{ xin: xout as number = 123 }
{ <number>x }
{ <number>xin: xout = 123 }
{ x<number> }
{ xin: xout<number> = 123 }
{...}: { x: number }
{ xin = 123 }: { xin: number? }
{ xin: xout }: { xin: number? }
{ xin: xout = 123 }: { xin: number? }
...{ x: number }
...{ xin xout: number = 123 }
{ x:: number}
{ xin: xout: number = 123 }
{ x:: number }
{ x:: number = 123 }
{ xin: xout:: number }
{ xin: xout:: number = 123 }
{ x number }
{ xin: xout number = 123 }
{ x number }
{ xin number: xout = 123 }
{ x: (number) }
{ xin: (xout: number) = 123 }

Pros and cons of each syntax

as_type

{ xin: xout as number = 123 }

Cons

  • as T is currently used for type assertion rather than type annotation; this will cause confusion

Pros

  • as T already means "type"; users will feel more familiar with this new syntax

angle_bracket_before

{ <number>xin: xout = 123 }

Cons

  • <T>x is currently used for type assertion rather than type annotation; this will cause confusion

Pros

  • <T>x already means "type"; users will feel more familiar with this new syntax

angle_bracket_after_remap

Cons

  • x<T> looks like A<T>, the syntax for type parameter; this overload can feel strange

Pros

  • x<T> feels like Typescript since it's similar to an existing syntax

auto_spread

{ xin: xout = 123 }: { xin: number? }, but {...}: { x: number } in the simple case

Cons

  • only the simple case receives the improvement
  • let {...}: { x: number } = a might let the user believe that let {...} = a exists and is a meaningful unpacking strategy, but it is not since the { ... } syntax only exists with type annotation

Pros

  • this syntax is very similar to existing syntaxes

spread_with_remap

Cons

  • let ...{ x: number } = a might let the user believe that let ...T exists and is a meaningful unpacking strategy, but it is not
  • it overloads type destructuring somewhat
  • it gives a new syntax to remapping -- differing from ES6's syntax

Pros

  • this syntax is very similar to existing syntaxes

double_colon

{ xin: xout: number = 123 }

Cons

  • in the (rare) case of variable remapping, the same separator is used between the remap and the type, which can make things harder to parse visually

Pros

  • the colon is already the current operator for type annotation; adding a second colon to disambiguate from variable remapping feels like an obvious thing to do

@mhegazy

  • there is value in consistency that types always come after a :. adding another operator would increase complexity.
  • we have the : domain almost officially reserved for types.

colon_square

{ xin: xout:: number = 123 }

(disallowing spaces between the two colon)

Cons

  • in effect, the "colon square" is a new operator for type annotation, since it is kept in the variable remapping syntax

Pros

  • it uses colons, which are the usual operator for type annotation

concatenation_after_remap

{ xin: xout number = 123 }

Cons

  • concatenation without a visual separator can be visually hard to parse
  • concatenation has never been used for type annotation in TypeScript

Pros

  • concatenation is simple

concatenation_before_colon

{ xin number: xout = 123 }

Cons

  • in the case of a remap: { xin number: xout }, the type number appears before the variable xout; which is never done in TypeScript

Pros

  • concatenation is simple

parentheses

{ xin: (xout: number) = 123 }

Cons

  • having parentheses around the type for disambiguation feels quite new: { xin: (number) }

Pros

  • the type always appears right of a colon
  • parenthesis are already used with types when grouping is needed

@mathieucaroff
Copy link

Here is my order of preference on these 10 alternatives, from most wanted to least wanted:

  • spread_with_remap
  • auto_spread
  • parentheses
  • colon_square
  • double_colon
  • --approval threshold--
  • concatenation_after_remap
  • angle_bracket_after_remap
  • as_type
  • concatenation_before_colon
  • angle_bracket_before

My "approval threshold" separates the options I would vote for from those I would not in the event of a vote using approval voting.

I'm interested in your own preferences, especially if you believe debating can lead to a consensus. In case debating fails to lead to a consensus, or debating is not attempted, I believe a vote (using approval voting) would still be a satisfying outcome.

@jwalkerinterpres
Copy link

How many people program in TypeScript in this world? How can only 30+ devs' opinions represent all of them? Because you can speak for every TS React dev on the planet?

Of course I can't; that was obviously a figure of speech.

But you're setting up an impossible standard: if any dev team (not just TS's) waited until they had consensus from every dev on the planet to make changes, no changes would ever get made.

What this thread has is the opinion of all the people who've cared enough to chime in, since 2019. I think that's a good group of people for the devs to listen to (while of course also using their own judgement).

@nyngwang
Copy link

nyngwang commented Apr 13, 2024

Let's focus on the topic :)

At the very least, we should be aware of the fact that the Flow team closed an identical issue last year facebook/flow#235 (comment): (bolded font style by me.)

We banned this completely last year, since it's confusing.

The long-lasting nature of both threads reflects the same point. And I always prefer the status-quo backed by the creator of TypeScript: (bolded font style by me.)

It's not the greatest, but at least it is clear what is going on. I'm concerned that proposals to introduce more overloaded meanings for : or as in destructuring patterns just make matters worse.

After all, we're not the maintainers. That's why we should respect every statement from the maintainers. Let's review one of the forgotten warnings clearly made years ago:

ultimately, every new syntax added is added complexity, for the user to learn and recognize, and for the compiler code base to maintain, so usually we try to limit complexity unless the value warrants it. and for destructuring, the syntax is already complex to start with, and we did not want to add yet another layer on top.

Now I have read the entire thread. I'm still confused about where is "the consensus". There are still at least 20% downvotes for the candidates above.

double_colon

{ x:: number}
{ xin: xout: number = 123 }

colon_square

{ x:: number }
{ xin: xout:: number = 123 }

At the very least, we should not use/occupy the potential :: operator from ECMAScript, just mentioned above by a member of TC39:

Even if they were, i wouldn’t want the bind operator, or something like it (like the much less stale call-this proposal) to be killed.

This invalidates both candidates.

We should always consider these unresolved caveats before "pushing the team" to add any feature irrationally.

@nyngwang
Copy link

nyngwang commented Apr 13, 2024

Let's investigate the problem statement in the original post:

But in Typescript a and b are untyped (inferred to have type any) and type annotation must be added (either to aid in type safety or to avoid compiler errors, depending on the state of the user's strict flags). To add the correct type annotation it feels natural to write:

const MyComponent = ({ a, b }) => {
    // ...
}

If we keep the one simple rule in mind: "Types are annotated at the top-level". We can do it right from the start:

//                           ┌>  We always mark the type outside of an object.
const MyComponent = ({ a, b }: { a: string, b: number }) => {
    // ...
}

By breaking this rule, everything becomes complicated:

//                        ┌>  The first case to mark types inside of an object.
//                        │   Prepare for the incoming syntax conflict with ECMAScript.
const MyComponent = ({ a :: string, b :: number }) => {
    // ...
}

On a second look, the candidates all encourage us to write disposable code.

Without it, the following code:

type Added = {...}

const MyComponent = ({ a, b }: { a: string, b: number } & Added) => {
    //                         └> The left operand of `&` is purely a type.
    //                            So it can be easily extracted.
    // ...
}

can be easily refactored into:

type Original = {...} // extracted. it can be easily reused.
type Added = {...}

const MyComponent = ({ a, b }: Original & Added) => {
    // ...
}

Now let's look at one of our candidates:

type Added = {...}

//                   ┌> We cannot extract the type.
//                   │  We will not write readable code when the # of props grows.
const MyComponent = ({ a :: string, b :: number } & Added) => {
    //                                            └> Is this even allowed?
    //                                               The left operand is not purely a type.
    // ...
}

Do we really need to increase the complexity of the compiler for the syntax? The optional rename-in-the-middle has not been included yet. The generic functions syntax has not been discussed in this thread yet.


Even more, I suspect that these candidates might not align with many of the Design Goals:

Goals

5. Produce a language that is composable and easy to reason about.
6. Align with current and future ECMAScript proposals.
11. Do not cause substantial breaking changes from TypeScript 1.0.

Non-goals

7. Introduce behaviour that is likely to surprise users. Instead have due consideration for patterns adopted by other commonly-used languages.

@pfeileon
Copy link

The only of these that doesn't sound completely unreasonable to read / use would be angle after match, i.e. function MyComponent({name<string>, age<number>}) { ... } still, I think overall it's pretty bad / confusing.

There's other ways to solve this issue though, for example using very short name, like function MyComponent(p: {name: string, age: string}) { return p.name } (possibly one could even use a character like $ for it as well)

Also I think it should at least be considered whether the object-style props are really the best way to do things from Reacts side, as those could potentially also be function parameters.

In general, the switch from class components to functional components should not make this any more verbose. Instead of writing this.name everywhere you're now writing props.name, which is more readable anyway.

So yes, this is probably an unpopular opinion, but given the options in this thread I feel the best one would really be to just avoid the destructuring altogether.

The essence of this post is that destructuring means that you replace props with { foo, bar } so the only sane way to add type information without running into problems somewhere down the line is to provide it in the same way.

Also on a personal note, I don't get how this:
({ foo: string, bar: number }) =>
is better than this:
Interface MyProps { foo: string; bar: number }
({ foo, bar }: MyProps) =>
In fact I'd argue that the former is significantly less readable and looks significantly less like ECMAScript with types.

@piranna
Copy link

piranna commented Apr 13, 2024

Agree with @pfeileon, the more i think on the topic, the less sense i see on defining the types directly online on the object destructured fields.

@nathggns
Copy link

I'd bet a lot of people are motivated to address this due to React, and it's interesting that Flow has just 'special cased' components in order to address this - https://medium.com/flow-type/announcing-component-syntax-b6c5285660d0

I still hope that TypeScript ends up with a general syntax for this, but I'd settle for a component syntax like Flow's

@nyngwang
Copy link

nyngwang commented Apr 14, 2024

https://medium.com/flow-type/announcing-component-syntax-b6c5285660d0

Code today 😴

type Props = $ReadOnly<{
  text?: string,
  onClick: () => void,
}>;

export default function HelloWorld({
  text = 'Hello!',
  onClick,
}: Props): React.MixedElement {
  return <div onClick={onClick}>{text}</div>;
}

Component Syntax 🔥

export default component HelloWorld(
  text: string = 'Hello!',
  onClick: () => void,
) {
  return <div onClick={onClick}>{text}</div>;
}

That looks shiny at first, but if you think again... 🙄:

-export default component HelloWorld(
+export default function HelloWorld(
  text: string = 'Hello!',
  onClick: () => void,
) {
  return <div onClick={onClick}>{text}</div>;
}

Why did those beautiful name: Type pairs come back? Let's review what was quoted above:

We banned this completely last year, since it's confusing.

By promoting the new component keyword, they essentially ban the destructuring syntax...

What if the problem is not TypeScript support for the destructuring syntax from the start, but the destructuring syntax/shorthand itself (when overused everywhere)?

@pfeileon
Copy link

I'd bet a lot of people are motivated to address this due to React, and it's interesting that Flow has just 'special cased' components in order to address this - https://medium.com/flow-type/announcing-component-syntax-b6c5285660d0

I still hope that TypeScript ends up with a general syntax for this, but I'd settle for a component syntax like Flow's

But... there is a "general" syntax for this in TypeScript. This whole thread is built on the strawman that there wasn't.

@ehaynes99
Copy link

ehaynes99 commented Apr 18, 2024

I'd bet a lot of people are motivated to address this due to React

There is nothing special about react components here. They're just functions that declaratively state a set of dependencies. Any function that returns something you could conceivably call a "service" is the same. That flow treated them as a special case is puzzling.

That looks shiny at first, but if you think again... 🙄:

-export default component HelloWorld(
+export default function HelloWorld(
  text: string = 'Hello!',
  onClick: () => void,
) {
  return <div onClick={onClick}>{text}</div>;
}

Think about... what? You've retained their exact syntax and just converted everything to ordinal parameters. Looks fine there, but if you think again... 🙄:

export const connect = async (
  protocol?: string | undefined,
  hostname?: string | undefined,
  port?: number | undefined,
  username?: string | undefined,
  password?: string | undefined,
  locale?: string | undefined,
  frameMax?: number | undefined,
  heartbeat?: number | undefined,
  vhost?: string | undefined,
): Promise<Connection> => // ...

You're literally making the case for this feature; it's desirable to have the types next to the values. "beautiful name: Type pairs" is exactly what everyone here is after. I'm not going to rehash everything I said here, but that's exactly the elegance of the feature in ECMA; ordinal parameters and named parameters are practically identical. Just add brackets, and you instantly have a much safer pattern.

TypeScript will make people's code type-safe. TypeScript will not make people's code confusing. All existing proposals are confusing.

Personally, I treat the parameter destructuring syntax as an anti-pattern. It's already a confusing shorthand syntax that looks like something half-done when you only rename a subset of the properties.

Destructuring is functionally identical to named parameters, which is a common feature in many languages. The overlap of places where it's appropriate is 100%.

Also on a personal note, I don't get how this:
({ foo: string, bar: number }) =>
is better than this:
Interface MyProps { foo: string; bar: number }
({ foo, bar }: MyProps) =>

You can't be serious. It's the same as how:

(foo: string, bar: number) => // ...

is better than this:

type MyProps = [string, number]

(...[foo, bar]: MyProps) => // ...

@nyngwang
Copy link

nyngwang commented Apr 18, 2024

@ehaynes99

Think about... what?

Think about why you insist on using destructuring syntax in the first place. Regarding the example you made, we can easily solve it without the destructuring syntax and propose any confusing :: operator:

type NetworkData = {
  protocol?: string | undefined,
  hostname?: string | undefined,
  port?: number | undefined,
  username?: string | undefined,
  password?: string | undefined,
  locale?: string | undefined,
  frameMax?: number | undefined,
  heartbeat?: number | undefined,
  vhost?: string | undefined,
}

export const connect = async (data: NetworkData): Promise<Connection> => // ...

it's desirable to have the types next to the values.

It's desirable for people who use destructuring syntax a lot. But the point of my comment above is:

they essentially ban the destructuring syntax...

By this line, I was asking "Did you see any destructuring syntax in the new component syntax?".


Regarding your comment above:

But since we can't go back and fix that, why not just replace the : we use for types with some other symbol like :: and leave everything else exactly as it is:

const makeRequest = async ({
  url:: string,
  method:: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'POST',
  headers:: Record<string, string> : thingsAtTheTop,
  body?:: any,
}) => {
  // ...
}

It's practically invisible. There wouldn't be some new construct adding complexity overhead, to the benefit of both the developer as well as for the compiler, all of the third-party parsers, formatters and other tooling.

Are you sure it's really a benefit for the compiler? I pointed out the potential problem above and your answer didn't help:

By breaking this rule, everything becomes complicated:

//                        ┌>  The first case to mark types inside of an object.
//                        │   Prepare for the incoming syntax conflict with ECMAScript.
const MyComponent = ({ a :: string, b :: number }) => {
    // ...
}

Without this proposal, the compiler doesn't have to check any type inside any value:

image

@AldoMX
Copy link

AldoMX commented Apr 21, 2024

I've been using default values to scratch that itch:

function Component({ children = null as ReactNode, price = 0, title = '' }) {
    // ...
}

@mick62
Copy link

mick62 commented May 24, 2024

// Keep the function definition clean:
function fun(foo: string, bar: string, baz: string) { ... }

// New syntax to (optionally) provide the parameter names in the call
x = fun(foo: 'a', bar: 'b', 'c')

// Syntax alternatives
x = fun(foo := 'a', bar := 'b', 'c')
x = fun(foo is 'a', bar is 'b', 'c')
x = fun(foo _ 'a', bar _ 'b', 'c')

Let's think out of the box.

  1. A language should avoid all technical clutter. Hence we should find a way without the need to use curly braces in the parameter list as well as on the call side only to have named parameters.
  2. There is no need to rename a parameter in the parameter list. This is only necessary in the different use case when a type is used which is already defined elsewhere.

The conclusion is that we should think about a syntax extension to provide the parameter names on the caller side (without resorting to an object). To avoid any problems to transpile to JS, the sequence of the parameters in the call must be the same as in the parameter list (as usual).

It would be nice to use a similar trick that is possible with objects

const foo = 'a'
const bar = 'b'
const bazar = 'c'

type Ex = { foo: string, bar: string }
const ex = { foo, bar, baz: bazar }

// Option: Combine parameter name and name of arg variable
x = fun(:foo, :bar, bazar)

// Alternatives
x = fun( _ foo, _ bar, bazar)
x = fun( (foo), (bar), bazar)

@jwalkerinterpres
Copy link

This ticket is full of ideas, after (literally) half a decade of discussion. With all due respect, I disagree that we need to:

think out of the box

We've thought outside the box, inside the box, and around the box. We've summarized the possibilities, discussed the trade-offs between them, and prioritized the top options.

At this point what we need isn't more thought, it's guidance from the TS team: what are they willing to accept as a PR to this ticket? Without that, we're all just squawking into the oblivion here.

@mick62
Copy link

mick62 commented May 25, 2024

This ticket is full of ideas, after (literally) half a decade of discussion. With all due respect, I disagree that we need to:
"think out of the box"

Then maybe we need to "take a step back" to concentrate on the core requirement.
Ultimately we want to be able to include the parameter names on the caller side.
The reason is to have better documentation and to avoid mistakes.

Using objects for this purpose is a poor mans work-around due to current syntactical limitations.
Actually this work-around is a misuse because it has a slight performance cost (which might add up).

The fact that "after (literally) half a decade of discussion" no progress is made shows that it's likely a dead end street.
Instead of adding syntax to support the work-around, we should add syntax to make the work-around unnecessary.

@PartMan7
Copy link

This ticket is full of ideas, after (literally) half a decade of discussion. With all due respect, I disagree that we need to:
"think out of the box"

Then maybe we need to "take a step back" to concentrate on the core requirement.
Ultimately we want to be able to include the parameter names on the caller side.
The reason is to have better documentation and to avoid mistakes.

Using objects for this purpose is a poor mans work-around due to current syntactical limitations.
Actually this work-around is a misuse because it has a slight performance cost (which might add up).

The fact that "after (literally) half a decade of discussion" no progress is made shows that it's likely a dead end street.
Instead of adding syntax to support the work-around, we should add syntax to make the work-around unnecessary.

I'd honestly love having a different syntax entirely for named parameters, but the issue is adding in named parameters would be a JavaScript thing, not a TypeScript project. Within the scope of TypeScript's goals, and in particular given how incredibly often destructuring patterns are used, I feel like we should have some syntax added for types in destructured parameters.

Also, the sooner the better for this; writing every single parameter multiple times is painful as a React developer.

@mick62
Copy link

mick62 commented May 25, 2024

I'd honestly love having a different syntax entirely for named parameters, but the issue is adding in named parameters would be a JavaScript thing, not a TypeScript project.

There is no change needed on JS side. The transpiler would simply throw away the additional parameter names:

fun(foo: 'a', bar: 'b', 'c') ---> fun('a', 'b', 'c')

@FeldrinH
Copy link

It's worth noting that type hints when destructuring are useful for more than named function parameters. For example Svelte 5 uses them for declaring types of component props (see https://svelte-5-preview.vercel.app/docs/runes#$props).

@FeldrinH
Copy link

FeldrinH commented May 25, 2024

There is no change needed on JS side. The transpiler would simply throw away the additional parameter names:

fun(foo: 'a', bar: 'b', 'c') ---> fun('a', 'b', 'c')

But what if you have function calls with different argument orders? For example, what if you have fun(foo: 'a', bar: 'b', 'c') in one place and fun(bar: 'b', foo: 'a', 'c') in another?

@mick62
Copy link

mick62 commented May 25, 2024

There is no change needed on JS side. The transpiler would simply throw away the additional parameter names:
fun(foo: 'a', bar: 'b', 'c') ---> fun('a', 'b', 'c')

But what if you have function calls with different argument orders? For example, what if you have fun(foo: 'a', bar: 'b', 'c') in one place and fun(bar: 'b', foo: 'a', 'c') in another?

The call fun(bar: 'b', foo: 'a', 'c') would be rejected by tsc because the sequence of the actual arguments must comply with the sequence of the formal parameters (as usual).

@PartMan7
Copy link

There is no change needed on JS side. The transpiler would simply throw away the additional parameter names:
fun(foo: 'a', bar: 'b', 'c') ---> fun('a', 'b', 'c')

But what if you have function calls with different argument orders? For example, what if you have fun(foo: 'a', bar: 'b', 'c') in one place and fun(bar: 'b', foo: 'a', 'c') in another?

The call fun(bar: 'b', foo: 'a', 'c') would be rejected by tsc because the sequence of the actual arguments must comply with the sequence of the formal parameters (as usual).

The issue is, named parameters are more than just for reference - one of the very important things they do is allow omitting parameters and being immune to order (such as configs, where you can have dozens of props where only 1-2 might be specified, or React components, where refectoring a component to add a new property lets you change the interface without having to rewrite every single instance).

@mick62
Copy link

mick62 commented May 25, 2024

The issue is, named parameters are more than just for reference - one of the very important things they do is allow omitting parameters (such as configs, where you can have dozens of props where only 1-2 might be specified, or React components, where refectoring a component to add a new property lets you change the interface without having to rewrite every single instance).

Yes, these two common use cases justify to objectify (at least the optional) parameters.

But there is no need to rename properties of the anonymous object type.
So neither x: y: string nor x: y:: string is needed.

If we restrict the object type to optional parameters we can use a special syntax to define the object type in a concise way:

fun(foo: string, ?{bar: string, baz: number})

@VanCoding
Copy link

@mick62 I also like named Parameters, especially how they are implemented for example in kotlin, where the order does not matter if you provide a name. But I don't think they are a replacement to object parameters as we're currently used to in JavaScript/TypeScript. Objects allow so much more things than is possible with named parameters, for example very complex mixins. If named parameters really should be the solution to the topic of the issue discussed here, then they would have to be very powerful, something that would have to be added to JavaScript first, and not something that can just be done in TypeScript.

And that needs a lot of time, if it's even possible to come up with something that's actually good. Additionally, all frameworks would have to make use of this feature first, like react, for example. Props objects would need to be migrated to this new named parameter feature, which is a bit unlikely.

And as long as we have very widely used stuff that requires object parameters like react, it's really desirable to have a TypeScript feature, like the one discussed here. Named parameters are no replacement to it. They would at best be complementary. And therefore, I'm nut sure it makes sense to discuss this here.

But nonetheless, I really like your thinking about this!

@otomad
Copy link

otomad commented May 26, 2024

There is no change needed on JS side. The transpiler would simply throw away the additional parameter names:

fun(foo: 'a', bar: 'b', 'c') ---> fun('a', 'b', 'c')

It's puzzle to use functions like forwardRef in React.

export default forwardRef(function MyInput({ foo, bar, baz }, ref) {

});

Although in React we can use <MyInput />, if we directly call a function like this, how to use it in your opinion?

MyInput(foo: 'a', bar: 'b', 'c')

or

MyInput(foo: 'a', bar: 'b', ref: 'c')

? Obviously both of the above are wrong.

@otomad
Copy link

otomad commented May 26, 2024

But there is no need to rename properties of the anonymous object type.
So neither x: y: string nor x: y:: string is needed.

If we restrict the object type to optional parameters we can use a special syntax to define the object type in a concise way:

fun(foo: string, ?{bar: string, baz: number})

In Vue, it uses prop name class instead of className. So if you declare a function according to your plan

function Fun(?{class: string}) {
    const classList = class.split(' ');
                      ^^^^^
}

class is a keyword, it can't be used as a variable inside the function anymore unless you rename it.
Although you can directly use prop name with className instead of class, users will feel it weird when they use HTML prop with class and your component prop with className.

@mick62
Copy link

mick62 commented May 26, 2024

export default forwardRef(function MyInput({ foo, bar, baz }, ref) { ...  });

Although in React we can use <MyInput />, if we directly call a function like this, how to use it in your opinion?

In the usual way (assuming 'baz' is optional):

MyInput({foo: 'a', bar: 'b'}, 'c')

I'm not in general against using an object to aggregate parameters where it makes sense like in your example.

But in my (back-end) coding work flow I often need to refactor functions by aggregating the separate parameters into an object just to have "named" parameters (for documentation and to prevent mix-up of arguments).
All the call sites then also need to be refactored. For this use case I want a simpler solution.

As VanCoding pointed out, it's complementary.

@PartMan7
Copy link

export default forwardRef(function MyInput({ foo, bar, baz }, ref) { ...  });

Although in React we can use <MyInput />, if we directly call a function like this, how to use it in your opinion?

In the usual way (assuming 'baz' is optional):

MyInput({foo: 'a', bar: 'b'}, 'c')

I'm not in general against using an object to aggregate parameters where it makes sense like in your example.

But in my (back-end) coding work flow I often need to refactor functions by aggregating the separate parameters into an object just to have "named" parameters (for documentation and to prevent mix-up of arguments).
All the call sites then also need to be refactored. For this use case I want a simpler solution.

As VanCoding pointed out, it's complementary.

I agree with this, but I will say that it is incredibly frustrating to use current types in cases where object destructuring is the way to go (which includes every single React component). Would be greatly appreciated if we could finally get a solution for this, and keep discussion open for ways ahead in the future about other ideas separately.

@ehaynes99
Copy link

Guys, you're discussing a completely different feature now that has nothing to do with this issue. Open another one if you want a new syntax for named parameters. But frankly I don't even see the point of this proposal. It's just ordinal parameters with labels that are then not even associated with variable names on the calling side. The calling side of destructured parameters is already perfect as-is IMO, but even if you disagree, the call signature was NEVER part of this issue.

Beyond that, this is not consistent with the design goal of adding a type system to JavaScript. It's completely unprecedented for TS to add syntax to identifiers, and violates design goal 8:

Avoid adding expression-level syntax.

You state:

Instead of adding syntax to support the work-around, we should add syntax to make the work-around unnecessary.

It's not a work around, it's a language feature added to ECMA a decade ago. This issue is to improve TypeScript's type type signature for it. This is not the place to debate that feature. Those discussions were concluded a decade ago.

@mick62
Copy link

mick62 commented May 28, 2024

Beyond that, this is not consistent with the design goal of adding a type system to JavaScript. It's completely unprecedented for TS to add syntax to identifiers, and violates design goal 8:

Avoid adding expression-level syntax.

IMO you overinterprete this design goal which would also disallow the expression x as string.
The expression for the argument is unchanged, only some context is added fun(foo: x).

My first post was a bit provocative :-) . Due to good arguments in follow-up answers I agree that we still need syntactic sugar to declare and use anonymous object types in function definitions.
Allthough someone might argue that a deconstructor expression in a function definition should be protected by goal 8 😉 .

So to get my wish I will create an ECMA proposal then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests