-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Comments
I know expression level syntax is a no-no, but how about: const MyComponent = ({ ... } : { a : string, b : number }) => {
// ...
} With the 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. |
I agree it's a duplicate but it really does suck. We should try again. |
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. |
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. |
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. |
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. |
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. |
Actually the more I think about // 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 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
}
}) => {
} |
Works for me! 👍 |
@dragomirtitian Good point! |
Were angle brackets considered? #34748 |
Has any progress been made on this? The current solution is not good since it is not DRY, e.g. |
DRY?
… On 9 Dec 2019, at 21:03, Chris Frolik ***@***.***> wrote:
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.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub <#29526?email_source=notifications&email_token=ABQZHXW5ME4SAYR634QZ7STQX2XCVA5CNFSM4GRUGZRKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEGKWCEA#issuecomment-563437840>, or unsubscribe </~https://github.com/notifications/unsubscribe-auth/ABQZHXWA7OFF26Q7FGJFUTLQX2XCVANCNFSM4GRUGZRA>.
|
Not really related, but it'd be much more easier if object destructuring alias uses |
@pynnl that ship sailed a long time ago unfortunately. |
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. |
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.
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 |
Here is another idea of syntax, which resembles the "Auto spread" proposition of @dragomirtitian: 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 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 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 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;
} |
Content
Vocabulary
All the issues I could find
Note:
Reason(s) to decline the feature request (back in 2016)
-- seconded by @DanielRosenwasser Proposition(s) with unusable syntaxHere 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)
{ x:: number }
{ xin:: number = 123 }
{ xin: xout: number }
{ xin: xout: number = 123 }
{ x :: number }
{ xin :: number = 123 }
{ xin: xout: number }
{ xin: xout: number = 123 }
{ x::number }
{ xin::number = 123 }
{ xin: xout: number }
{ xin: xout: number = 123 }
{ x::number }
{ xin::number = 123 }
{ xin:xout:number }
{ xin:xout:number = 123 } Typescript 3.9.2 (current situation)
{ x }: { x: number }
{ xin = 123 }: { xin: number }
{ xin: xout }: { xin: number }
{ xin: xout = 123 }: { xin: number }
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 findAll 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
|
Here is my order of preference on these 10 alternatives, from most wanted to least wanted:
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. |
Let's investigate the problem statement in the original post:
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:
|
The essence of this post is that destructuring means that you replace Also on a personal note, I don't get how this: |
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. |
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 |
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
By promoting the new What if the problem is not TypeScript support for the destructuring syntax from the start, but the destructuring syntax/shorthand itself (when overused everywhere)? |
But... there is a "general" syntax for this in TypeScript. This whole thread is built on the strawman that there wasn't. |
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.
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
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%.
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) => // ... |
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 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 for people who use destructuring syntax a lot. But the point of my comment above is:
By this line, I was asking "Did you see any destructuring syntax in the new Regarding your comment above:
Are you sure it's really a benefit for the compiler? I pointed out the potential problem above and your answer didn't help:
Without this proposal, the compiler doesn't have to check any type inside any value: |
I've been using default values to scratch that itch: function Component({ children = null as ReactNode, price = 0, title = '' }) {
// ...
} |
// Keep the function definition clean: // New syntax to (optionally) provide the parameter names in the call // Syntax alternatives Let's think out of the box.
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' type Ex = { foo: string, bar: string } // Option: Combine parameter name and name of arg variable // Alternatives |
This ticket is full of ideas, after (literally) half a decade of discussion. With all due respect, I disagree that we need to:
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. |
Then maybe we need to "take a step back" to concentrate on the core requirement. Using objects for this purpose is a poor mans work-around due to current syntactical limitations. The fact that "after (literally) half a decade of discussion" no progress is made shows that it's likely a dead end street. |
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. |
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 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). |
But what if you have function calls with different argument orders? For example, what if you have |
The call |
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). |
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. 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}) |
@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! |
It's puzzle to use functions like export default forwardRef(function MyInput({ foo, bar, baz }, ref) {
}); Although in React we can use MyInput(foo: 'a', bar: 'b', 'c') or MyInput(foo: 'a', bar: 'b', ref: 'c') ? Obviously both of the above are wrong. |
In Vue, it uses prop name function Fun(?{class: string}) {
const classList = class.split(' ');
^^^^^
}
|
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). 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. |
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:
You state:
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. |
IMO you overinterprete this design goal which would also disallow the expression 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. So to get my wish I will create an ECMA proposal then. |
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:
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:but that's not what the user thinks due to the aforementioned syntax clash. The only valid syntax in Typescript is actually this:
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:
Although this is really the only place it would be used, for the sake of consistency, I think is should be allowed everywhere:
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:
Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: