Repository navigation
Easier destructuring with type annotations on binding patterns #29526
Description
Activity
j-oliveras commented
on Jan 22, 2019 ContributorMore actionsdragomirtitian commented
on Jan 22, 2019 ContributorMore actionsI 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.
Reacted by Gy, Olmo del Corral, Georgii Dolzhykov, Mathieu CAROFF, Jerry Sky, AwesomeObserver, WJH, Amit Beckenstein, Ignacio Matias Haeussler, Andrei Alecu and 34 moreReacted by Brian KimReacted by Artem Sapegin, Dzianis Sudas, Darvesh, DominoPivot, Thomas, nyngwang, Kyle Venn and Ahmet Berk TAŞReacted by Zongli Shi- addedSuggestionAn idea for TypeScriptAn idea for TypeScriptIn DiscussionNot yet reached consensusNot yet reached consensus
on Jan 23, 2019 RyanCavanaugh commented
on Jan 23, 2019 MemberMore actionsI agree it's a duplicate but it really does suck. We should try again.
Reacted by Gy, Georgii Dolzhykov, Andreas Modahl, Chris Frolik, Jerry Sky, Axel Rauschmayer, sauln, AwesomeObserver, Braden Snell, julian and 38 moreReacted by Titian Cernicova-Dragomir, Matt Zeunert, Olmo del Corral, TazmanianD, David Hong, Gy, Richard Gibson, Georgii Dolzhykov, Andreas Modahl, Marek Ulicny and 22 moreReacted by Georgii Dolzhykov, Andreas Modahl, Jerry Sky, AwesomeObserver, Amit Beckenstein, douugdev, Eliaz Bobadilla, Hunter Wilhelm, Ahmet Berk TAŞ, Gustavo and 2 moreReacted by Georgii Dolzhykov, Andreas Modahl, Jerry Sky, Eliaz Bobadilla, Hunter Wilhelm, Jonathan MASSUCHETTI, nyngwang, Ahmet Berk TAŞ and Gustavo- changed the title
[-]A new sigil for specifying type when deconstructing[/-][+]Destructuring with type annotations (take 2)[/+]on Jan 27, 2019 - changed the title
[-]Destructuring with type annotations (take 2)[/-][+]Easier destructuring with type annotations on binding patterns[/+]on Jan 27, 2019 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.
Reacted by Georgii Dolzhykov, Saurabh Gupta, AwesomeObserver, Daniel Liuzzi, Eliaz Bobadilla, Murilo Schünke, Eric Pedley, David Rodrigues, Adrian, Luke Deen Taylor and 3 moreThere 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.
Reacted by TazmanianD, Danielle Burgess, Hunter Wilhelm, Aylon Carrijo, Gustavo and Javier CasaubónReacted by nyngwangYeah, 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.
Reacted by GustavoThe 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.
Reacted by nyngwang and d4krisWhile I also think Titian Cernicova-Dragomir (@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.
Reacted by Nick Cox, Randy Creasi, Richard Willis, Tamanon Virulhakieat, Corentin Forler, Loïc Bertrand, while(true);, Zach Hardesty, e, Tristan Hessell and 4 moreReacted by Brian Kimdragomirtitian commented
on Jul 31, 2019 ContributorMore actionsActually 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 } }) => { }
Reacted by Jordi Oliveras Rovira, Georgii Dolzhykov, Andreas Modahl, Nick Cox, tienvudev, Andrew Reeman, Randy Creasi, Julien Sanchez, Yuriy Burychka, Brian Schlenker and 51 moreReacted by Yuriy Burychka, Brian Schlenker, Tema Smirnov, Loïc Bertrand, Andreas Zetterlund, Christian Meredith, RickoNoNo3, Tarjei Skjærset, Luke Deen Taylor, Alex C R and 8 moreReacted by Georgii Dolzhykov, Andreas Modahl, tienvudev, Randy Creasi, Tema Smirnov, Nicolaos Skimas, vmenge, RickoNoNo3, Alex Olshansky, Tarjei Skjærset and 7 moreReacted by Amit Beckenstein, Ashley Claymore, Tarjei Skjærset, Alex C R, Dylan Piercey and Hunter WilhelmTitian Cernicova-Dragomir (@dragomirtitian)
Let's not consider :: as a new way to introduce a type annotation, but rather an empty rename.
Works for me! 👍
Reacted by Titian Cernicova-Dragomir, Olmo del Corral, Andreas Modahl, tienvudev, Randy Creasi, Corentin Forler, WJH, Byron Broughten, Andreas Zetterlund, Hunter Wilhelm and 2 moreTitian Cernicova-Dragomir (@dragomirtitian) Good point!
131 remaining items
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')
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).
Reacted by Juhan Oskar HennosteThere 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 andfun(bar: 'b', foo: 'a', 'c')in another?Reacted by PartMan, Tomas Zaluckij and GustavoThere 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 andfun(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).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 andfun(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).
Reacted by Juhan Oskar Hennoste, Tomas Zaluckij, Eric Haynes, Gustavo and d4krisThe 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 neitherx: y: stringnorx: y:: stringis 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})
Reacted by Eric Haynesmick62 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!
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
forwardRefin 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.
But there is no need to rename properties of the anonymous object type.
So neitherx: y: stringnorx: y:: stringis 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
classinstead ofclassName. So if you declare a function according to your planfunction Fun(?{class: string}) { const classList = class.split(' '); ^^^^^ }
classis 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 withclassNameinstead ofclass, users will feel it weird when they use HTML prop withclassand your component prop withclassName.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.
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.
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.
Reacted by Juhan Oskar Hennoste, Anton Bessonov, Spenser Black, Tomas Zaluckij, PartMan, otomad, Jordan Harband, Nicolaos Skimas, snarbies, nyngwang and 2 moreBeyond 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 addedfun(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.
Reacted by Kyle Venn, Manav Bokinala and Eduardo D SanchezI want to support this issue. This example is exactly what I think should be possible:
const MyComponent = ({ a : string, b : number }) => { // ... }I found the issue by looking for the issue duplicate before filling my ticket about such structure.
I think this example is so natural, that everybody would understand it even with no additional documentation. I do not say to skip it in the docs, just I want to highlight how intuitive this pattern is.
Technically, I would say that this is a mix of inferred type and typing with annotations. When writing this I literally look at this part of TS docs (source):
When you declare a variable using const, var, or let, you can optionally add a type annotation to explicitly specify the type
of the variable:let myName: string = "Alice";In most cases, though, this isn’t needed. Wherever possible, TypeScript tries to automatically infer the types in your code. > For example, the type of a variable is inferred based on the type of its initializer:
// No type annotation needed -- 'myName' inferred as type 'string' let myName = "Alice";Comparing to documentation we can say that whole
MyComponentprops are inferred from the destructure statement, howevera, andbhave type annotations.This is just about allowing type annotations inside destructure statement, and applying them to inferred type of whole component props.
The big influence on programming style would be that writing these function signatures would need almost the same effort:
function example(a: string, b?: string) function example({ a: string, b?: string })In situation when there is a need to add required argument
cfor first function, due to requirement of having optional args at the end it becomes:function example(a: string, c: string, b?: string)As the result all the function calls must be fixed, and if this is a public api of a library, then it implies a major version release.
For the second version the modified signature will become
function example({ a: string, b?: string, c: string })which needs no additional changes in the rest of the code, and minor version release if it is a lib api.
Due to this advantage, and minimally more effort, I expect
function example({ a: string, b?: string })variant to be used more often. Because of this I expect this little modification of TS to have significant positive impact on velocity and maintenance costs of many projects.
I would not only allow this structure in function signatures. Example usages outside a function signature:
// both a and b with annotated types const [a: string, b: string] = fn(...args); // both a with annotated type, and b with inferred type const [a: string, b] = fn(...args); // both a with annotated type, and rest with inferred type const [a: string, ...rest] = fn(...args); // both a and rest with annotated types const [a: string, ...rest: number[]] = fn(...args); const { a: string, b: string } = fn(...args); const { a: string, b } = fn(...args); const { a: string, ...rest: Record<string, number> } = fn(...args); // a with annotated type, b with inferred type which includes annotated c property const { a: string, b: { c: string } } = fn(...args);This is not so often needed, but I found myself in need of typing the destructure result when
fnhas many alternative signatures, and its result is not correctly inferred from theargs.
I want to notice that after this change, these would be equivalents, but I find this fact neutral:
function example(a: string, b?: string) function example([a: string, b?: string])I do not expect the second version to really show up in code as it is simply longer to write.
const MyComponent = ({ a : string, b : number }) => { // ... }This is an ambiguous syntax, for it could also be interpreted as "deconstructing the argument as an object, with its
arenamed tostringand itsbrenamed tonumber"I suggest this:
const MyComponent = ({ (a : string), (b : number) }) => { // ... }if the exported variables need to be both annotated and renamed, it turns to be
const MyComponent = ({ (a : string) : x, (b : number) : y }) => { // ... }
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: