Typescript: Destructuring with type annotations

Created on 18 Mar 2016  ·  22Comments  ·  Source: microsoft/TypeScript

Using destructuring in functions signatures is a relief, but when combining it with type declarations there is a lot of duplication necessary:

function foo(
  {paramA, paramB, paramC, paramD = null}
  :{paramA: number, paramB: string, paramC: boolean, paramD?: Object}
) {}

function bar(
  [paramA, paramB, paramC, paramD]
  :[number, string, boolean, Object]
) {}

Would it make sense to introduce the following syntax to reduce duplication of code?

function foo(
  {paramA as number, paramB as string, paramC as boolean, paramD? as Object}
) {}

function bar(
  [paramA as number, paramB as string, paramC as boolean, paramD as Object]
) {}

Originally I wanted to suggest to use the colon ":" instead of the "as", but since the colon is used for renaming destructured parametes, the "as" is the only option I guess.

With this syntax change the following would be possible too I guess:

let {paramA as number, paramB as string, paramC as boolean, paramD as Object} = anyObject;
let {paramA: a as number, paramB: b as string, paramC: c as boolean, paramD: d as Object} = anyObject;
let [paramA as number, paramB as string, paramC as boolean, paramD as Object] = anyArray;
Declined Suggestion

Most helpful comment

Naming parameters via object literals is an increasingly popular pattern in JavaScript. TC39 even abandoned real named parameters in favor of this pattern.

Therefore, TypeScript should really support it as well as it can. This is how it could be done:

// No type annotation
{ x }
{ x = 123 }
{ xin: xout }
{ xin: xout = 123 }

// Type annotation via parentheses
{ (x: number) }
{ (x: number) = 123 }
{ xin: (xout: number) }
{ xin: (xout: number) = 123 }

// Type annotation via `as` operator
{ x as number }
{ x as number = 123 }
{ xin: xout as number }
{ xin: xout as number = 123 }

Compare:

// Current TypeScript:
function func({firstProp, secondProp, thirdProp}:
  {firstProp: number, secondProp: number, thirdProp: number} = {}) {
  /* ··· */
}

// Better support for named parameters:
function func({firstProp as number, secondProp as number, thirdProp as number} = {}) {
  /* ··· */
}

The latter notation is much closer to the “simulated named parameters” idea.

Real-world example:

Current TypeScript (“Showoff” is the name of my slide framework):

function slidesToNodeTree(
  {conf, configShowoff, inputDir, slideDeckDir, slideFileName, parentPartNode, visitedSlideFiles}
  : {conf: ConfigSlideLink, configShowoff: ConfigShowoff, inputDir: string,
    slideDeckDir: ServePath, slideFileName: string, parentPartNode: PartNode,
    visitedSlideFiles: SlideFileDesc[]}) {
  ···
}

With a better notation:

function slidesToNodeTree(
  { conf as ConfigSlideLink, configShowoff as ConfigShowoff, inputDir as string,
    slideDeckDir as ServePath, slideFileName as string, parentPartNode as PartNode,
    visitedSlideFiles as SlideFileDesc[]}) {
  ···
}

Alternatively:

function slidesToNodeTree(
  { (conf: ConfigSlideLink), (configShowoff: ConfigShowoff), (inputDir: string),
    (slideDeckDir: ServePath), (slideFileName: string), (parentPartNode: PartNode),
    (visitedSlideFiles: SlideFileDesc[])}) {
  ···
}

Once again: less redundancy and the parameters are directly connected with their types.

Similar issue with multiple return values:

Example in my code:

  • parseIndexFromDir() returns multiple values, but we are only interested in pageNode here.
  • I wanted to add a type annotation, to make things explicit.
// Current TypeScript:
const {pageNode}: {pageNode: PageNode} = parseIndexFromDir(···);

// With better syntax:
const {pageNode as PageNode} = parseIndexFromDir(···);

// Alternatively:
const { (pageNode: PageNode) } = parseIndexFromDir(···);

All 22 comments

We have talked about this as a possibility during the design of the destructuring support. a few thoughts,

  • as should have been using for the rename operator instead of :, that maps well to the import case where as is used to rename an import or export name binding, e.g. import {a as b} from "m" or import * as ns from "m".
  • there is value in consistency that types always come after a :. adding another operator would increase complexity.
  • there is a possibility of the committee (TC39) using as in the future, again to align with imports as an alternative for renaming. at this point we would be in a bind. we have the : domain almost officially reserved for types.
  • the compiler will infer the types from initialization, and we have done multiple iterations of improvements on the inference logic, to make sure you do not need to type out a type unless you have to, see https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#improved-checking-for-destructuring-object-literal for more details.
  • 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.

I think we're going to need to decline this one. Adding any more syntax here would put us in a tough spot, and on the whole, I think this would confuse users quite a bit.

I have to second this request. Without some solution to this issue, I'm strongly inclined to avoid parameter destructuring in TypeScript even if it means going back to the days of myMethod(param1, null, null, 2, null, null, true). It's just too much overhead to maintain two separate lists of parameters, one for types and another for default values.

Note that this issue combined with #5326 make destructuring extremely verbose for class constructors, negating a ton of improvements I've made to my ES6 code that I'd lose moving to TypeScript.

@cletusw --noImplicitAny is fully optional. You can use parameter destructuring and all of the properties will be of type any, just like they would had your written your code for Babel. So you would lose absolutely nothing.

This is true, although I'm relying heavily on noImplicitAny to help me find all the places that need types, so I would be loathe to turn that off.

@MichaelBuen I fully agree. The rename syntax is not only awkward and incompatible it is also inconsistent with imports which obviously are a natural analogy even though they have different semantics.

It is also not that the committee didn't envision TypeScript or Flow, as typed ECMAScript dialects existed before the standard was finalized.

As best I can tell, the reason for using the : comes from a desire for symmetry between composing and decomposing object literals.

How about this?

function foo({surname string, birthDate Date})

If a code..

function foo(dto: {surname: string, birthDate: Date})

.. need to be improved to use destructured object, instead of this proposed syntax..

function foo({(surname: string), (birthDate: Date)})

.. which entails going to each variable and surrounding each of them with parenthesis, it's tiresome and makes the code looks so busy. It would be easier to just remove the colon:

function foo({surname string, birthDate Date})

And when it's need to be renamed, just use the already established syntax for renaming:

function foo({surname string: apellido, birthDate Date: fecha})

Maybe ES6 committee decided that renaming destructured object would be the norm, so they choose a terse syntax to rename something. But why ES6 didn't dream of JavaScript of having types someday, or having TypeScript being part of the browser? Had they thought of it, they would not choose colon for renaming variable, they would use as for renaming, which is symmetrical with import statement. Another alternative would be is not to use an operator at all, same with SQL:

SELECT * FROM Person as p

Not using as would be just as fine:

SELECT * FROM Person p

So renaming surname to apellido would be:

function foo({surname apellido, birthDate fecha})

const {surname apellido, birthDate fecha} = person;

And then when in the not-so-distant future that JavaScript would have types. Boom! they could just use the established syntax for declaring types:

function foo({surname apellido: string, birthDate fecha: Date})

const {surname apellido: string, birthDate fecha: Date} = person;

function foo({surname: string, birthDate: Date})

const {surname: string, birthDate: Date} = person;

Too late.

Will just wait for WebAssembly to take off, and wait for a language that has a nicer syntax :) It might also be our chance for a language to have a decimal type.

I highly suggest this syntax..

function foo({surname string, birthDate Date})

..it's not really weird to remove an operator for declaring types, e.g.

CREATE TABLE Person (
    PersonID  int,
    LastName  varchar(255),
    FirstName varchar(255),
    Address   varchar(255),
    City      varchar(255) 
);

Another advantage of the above over this one..

function foo({surname as string, birthDate as Date})

..is if ECMAScript committee decided to make the renaming use the as keyword so there's symmetry with import's as, this won't cause breaking change:

function foo({surname string as apellido, birthDate Date as fecha})

function foo({surname as apellido, birthDate as fecha})

const {surname string as apellido, birthDate Date as fecha} = person;

const {surname as apellido, birthDate as fecha} = person;

Whereas this one would cause breaking change when as would be introduced for renaming destructured object. Both surname and birthDate would have any type when as would be used for renaming in the future:

function foo({surname as string, birthDate as Date})

const {surname as string, birthDate as Date} = person;

Indeed, destructuring using colon operator gives symmetry for composing and decomposing between object literals.

However, when I first use destructuring it felt reversed:

const person = {lastname: 'Eich', birthDate: Date()};

const {lastname: apellido, birthDate} = person;

console.log(apellido);

I was expecting the new name would be on left, and is symmetrical with how the object is composed:

const {apellido: lastname, birthDate} = person;

Perhaps they decided that it would be easier for users to scan/read all the object's original property names when they are on left. And they would like the users to mentally read the colon as as.

Personally I also think as would have been better. I had the same intuition, thinking the names were backward. Then again maybe it's because I was used to looking at types in that position. Regardless, I see no reason why the syntax is different than that used for aliasing imports...

Related Flow issue facebook/flow/issues/235

So much pain right now to use destructuring with --noImplicitAny or --strict.

Plain space separation could be more readable if type would prepend name, like in C or Dart.
Added <> parentheses, would add readability too. It's better localized than (name:type):label and already used in type casting.

const Component = ({ count:c, max:m }: { count:number, max:number }) => <div>{c}/{m}</div>;
const Component = ({ count number :c, max number :m }) => <div>{c}/{m}</div>;

```tsx
const Component = ({ number count:c, number max:m }) =>

{c}/{m}
;
const Component = ({ count:c, max:m }) =>
{c}/{m}
;

Or just use a second colon `name[:[label][:type]]` as some functional languages do:
```tsx
const Component = ({ count:c:number, max:m:number }) => <div>{c}/{m}</div>;
const Component = ({ count::number, max::number }) => <div>{count}/{max}</div>;

@garkin

Ultimately, I think it is best to write

const Component = ({count, max}: Props) => <div>{count}/{max}</div>;
type Props = {count: number, max: number};

from a maintainability perspective.

Plain space separation could be more readable if type would prepend name, like in C or Dart.

Obviously this is subjective, but name: type has many advantages over type name from many different angles. There are many reasons for this, but Eric Lippert makes a good case for it so check out item 5 on this list

Regardless, while I definitely meant what I said about the inline rename syntax being confusing and unfortunate, the ship has long since sailed and so this is not going to change.

React components are a perfect example for destructuring, since props interfaces are never reused and keeping them separate smears the description and pollutes namespace. Latter could be fixed by wrapping them in namespace ComponentName {}, but it still adds more code to read.

I find I avoid destructuring ad hoc parameters in TypeScript because of this. Maybe good to define common types, but I find instead that I use indexed parameters more than I should instead. The decision to leave this out harms code quality, in my view.

I was exploring options with React and wanted to add what I had found in my experiments:

// Destructuring as an arg
// Pros:
// - Type Inference!
// - Same as ES6!
// - IDEAL!
export const C2 = ({ def = 'DEFAULT' }) => {
    // const opt: string (optional? lost: see NOTE above)
    return (<Text>{def}</Text>);
};

export const C2_Usage = () => {
    return (<C2></C2>);
};

// Unused Default Object (HACKY BUT GOOD) - WINNER FOR REACT!!!
// Pros:
// - Forces default value for all optional arguments
// - ES6 Compatible
// - Closest to IDEAL solution when all arguments are optional
// Cons:
// - Pseudo-Required Arguments (Works fine for React Components)
// - Requires Duplication of required variable names
export const D3 = ({ req, opt = '', def = 'DEFAULT' } = { req: '' }) => {
    // const opt: string (optional? lost: see NOTE above)
    return (<Text>{req + opt + def}</Text>);
};

export const D3_Usage = () => {
    return (<D3 req='REQUIRED_TRUE'></D3>);
};

export const D3_Call = () => {
    // CON: This is Possible
    D3();
    // But if any object is given, it works right
    D3({ req: '' });
};

This pattern will work well for React components, but it doesn't work great for normal functions because it's possible to call with no argument object.

Naming parameters via object literals is an increasingly popular pattern in JavaScript. TC39 even abandoned real named parameters in favor of this pattern.

Therefore, TypeScript should really support it as well as it can. This is how it could be done:

// No type annotation
{ x }
{ x = 123 }
{ xin: xout }
{ xin: xout = 123 }

// Type annotation via parentheses
{ (x: number) }
{ (x: number) = 123 }
{ xin: (xout: number) }
{ xin: (xout: number) = 123 }

// Type annotation via `as` operator
{ x as number }
{ x as number = 123 }
{ xin: xout as number }
{ xin: xout as number = 123 }

Compare:

// Current TypeScript:
function func({firstProp, secondProp, thirdProp}:
  {firstProp: number, secondProp: number, thirdProp: number} = {}) {
  /* ··· */
}

// Better support for named parameters:
function func({firstProp as number, secondProp as number, thirdProp as number} = {}) {
  /* ··· */
}

The latter notation is much closer to the “simulated named parameters” idea.

Real-world example:

Current TypeScript (“Showoff” is the name of my slide framework):

function slidesToNodeTree(
  {conf, configShowoff, inputDir, slideDeckDir, slideFileName, parentPartNode, visitedSlideFiles}
  : {conf: ConfigSlideLink, configShowoff: ConfigShowoff, inputDir: string,
    slideDeckDir: ServePath, slideFileName: string, parentPartNode: PartNode,
    visitedSlideFiles: SlideFileDesc[]}) {
  ···
}

With a better notation:

function slidesToNodeTree(
  { conf as ConfigSlideLink, configShowoff as ConfigShowoff, inputDir as string,
    slideDeckDir as ServePath, slideFileName as string, parentPartNode as PartNode,
    visitedSlideFiles as SlideFileDesc[]}) {
  ···
}

Alternatively:

function slidesToNodeTree(
  { (conf: ConfigSlideLink), (configShowoff: ConfigShowoff), (inputDir: string),
    (slideDeckDir: ServePath), (slideFileName: string), (parentPartNode: PartNode),
    (visitedSlideFiles: SlideFileDesc[])}) {
  ···
}

Once again: less redundancy and the parameters are directly connected with their types.

Similar issue with multiple return values:

Example in my code:

  • parseIndexFromDir() returns multiple values, but we are only interested in pageNode here.
  • I wanted to add a type annotation, to make things explicit.
// Current TypeScript:
const {pageNode}: {pageNode: PageNode} = parseIndexFromDir(···);

// With better syntax:
const {pageNode as PageNode} = parseIndexFromDir(···);

// Alternatively:
const { (pageNode: PageNode) } = parseIndexFromDir(···);

W.r.t. “the colon should really be as”: I don’t think of object destructuring as renaming, more like a transparent sheet that you put on top of data. That is:

  • My interpretation:

    • {x: y} extracts property x and assigns it to variable y

    • {x} is syntactic sugar for {x: x}

  • Versus:

    • {x: y} renames x to y

This interpretation makes even more sense if you nest destructurings:

let {b: {b, c}} = obj;

b is not renamed, but further destructured.

@rauschma I hope we can see this soon!

How about syntax like this?

function func({{  // use double curly brace
  a: number,
  b: string = 'x',
  c as d: boolean,
  e as {x1, y1}: {x1: number, y1: number},
  f as {{x2: number, y2: number}},
}}) { ... }

In current TypeScript, it means:

function func({
  a,
  b = 'x',
  c: d,
  e: {x1, y1},
  f: {x2, y2},
}: {
  a: number,
  b?: string,
  c: boolean,
  e: {x1: number, y1: number},
  f: {x2: number, y2: number},
}) { ... }

I think it don't break existing syntax and it's not so strange syntax.

Last update on this topic:

@odiak Interesting Idea

How about syntax like this?

function func({{ // use double curly brace
a: number,
b: string = 'x',
c as d: boolean,
e as {x1, y1}: {x1: number, y1: number},
f as {{x2: number, y2: number}},
}}) { ... }

However, I don't understand the purpose of writing c, e, and f that are immediately destructured. And I certainly don't like using as for anything but type casting.

Maybe something like:

function func({{ 
  // Type Only (Required Parameter)
  a: number,

  // Destructuring with no defaults (Required Parameter)
  {{b1:number, b2:number}},

  // Default Value (Optional Parameter)
  c = 'x',

  // Destructuring with default or optional values (Optional Parameter)
  {{d1 = 0, d2 = 0, d3 : number?}},

}}) { ... }

@ricklove

I don't understand the purpose of writing c, e, and f that are immediately destructured.

Because it's object destructuring, you need to specify name of parameters.
So 2nd and 4th argument on your example has no name. Maybe it cannot be destructured.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jbondc picture jbondc  ·  3Comments

siddjain picture siddjain  ·  3Comments

bgrieder picture bgrieder  ·  3Comments

dlaberge picture dlaberge  ·  3Comments

CyrusNajmabadi picture CyrusNajmabadi  ·  3Comments