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;
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"
.:
. adding another operator would increase complexity.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.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 }) =>
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.// 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:
{x: y}
extracts property x
and assigns it to variable y
{x}
is syntactic sugar for {x: x}
{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.
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:
Compare:
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):
With a better notation:
Alternatively:
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 inpageNode
here.