Skip to content

Easier destructuring with type annotations on binding patterns #29526

Description

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.

Activity

  1. j-oliveras commented on Jan 22, 2019

    @j-oliveras
    Contributor

    Duplicate/related to #7576, #29019

  2. dragomirtitian commented on Jan 22, 2019

    @dragomirtitian
    Contributor

    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.

  3. RyanCavanaugh commented on Jan 23, 2019

    @RyanCavanaugh
    Member

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

  4. changed the title [-]A new sigil for specifying type when deconstructing[/-] [+]Destructuring with type annotations (take 2)[/+] on Jan 27, 2019
  5. changed the title [-]Destructuring with type annotations (take 2)[/-] [+]Easier destructuring with type annotations on binding patterns[/+] on Jan 27, 2019
  6. olmobrutall commented on Feb 6, 2019

    @olmobrutall

    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.

  7. scottmas commented on Apr 30, 2019

    @scottmas

    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.

  8. TazmanianD commented on May 23, 2019

    @TazmanianD

    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.

  9. gynet commented on Jul 31, 2019

    @gynet

    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.

  10. Richiban commented on Jul 31, 2019

    @Richiban
    Author

    While 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.

  11. dragomirtitian commented on Jul 31, 2019

    @dragomirtitian
    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
      }
    }) => {
      
    }
  12. Richiban commented on Jul 31, 2019

    @Richiban
    Author

    Titian Cernicova-Dragomir (@dragomirtitian)

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

    Works for me! 👍

  13. gynet commented on Aug 2, 2019

    @gynet
  14. 131 remaining items

  15. mick62 commented on May 25, 2024

    @mick62

    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')

  16. FeldrinH commented on May 25, 2024

    @FeldrinH

    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).

  17. FeldrinH commented on May 25, 2024

    @FeldrinH

    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?

  18. mick62 commented on May 25, 2024

    @mick62

    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).

  19. PartMan7 commented on May 25, 2024

    @PartMan7

    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).

  20. mick62 commented on May 25, 2024

    @mick62

    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})

  21. VanCoding commented on May 25, 2024

    @VanCoding

    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!

  22. otomad commented on May 26, 2024

    @otomad

    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.

  23. otomad commented on May 26, 2024

    @otomad

    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.

  24. mick62 commented on May 26, 2024

    @mick62
    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.

  25. PartMan7 commented on May 26, 2024

    @PartMan7
    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.

  26. ehaynes99 commented on May 27, 2024

    @ehaynes99

    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.

  27. mick62 commented on May 28, 2024

    @mick62

    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.

  28. kneczaj commented on Nov 28, 2025

    @kneczaj

    I 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 MyComponent props are inferred from the destructure statement, however a, and b have 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 c for 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 fn has many alternative signatures, and its result is not correctly inferred from the args.


    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.

  29. AlseinX commented on Nov 28, 2025

    @AlseinX
    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 a renamed to string and its b renamed to number"

    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 }) => {
        // ...
    }
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions