Skip to content
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

Feature Request: Add labels to tuple elements #28259

Closed
4 tasks done
brainkim opened this issue Oct 31, 2018 · 29 comments · Fixed by #38234
Closed
4 tasks done

Feature Request: Add labels to tuple elements #28259

brainkim opened this issue Oct 31, 2018 · 29 comments · Fixed by #38234
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@brainkim
Copy link

brainkim commented Oct 31, 2018

Search Terms

tuple type elements members labels names naming

Suggestion

Currently tuple types are defined like so:

// length, count
type Segment = [number, number];

I often find myself having to add labels to the elements of the tuple as a comment b/c the types themselves (number, string) don't adequately describe what the elements represent.

It would be nice if we could add labels to tuple elements using syntax similar to function parameters like so:

type Segment = [length: number, count: number];

Use Cases

Currently we use comments to describe tuples, but adding this information directly to the AST can provide additional information for tooling.

Examples

This new syntax would also be useful in return types:

function createSegment(/* ... */): [length: number, count: number] {
  /* ... */
}

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. new expression-level syntax)
@brainkim brainkim changed the title Adding labels to tuple elements Oct 31, 2018
@brainkim brainkim changed the title Add labels to tuple elements Oct 31, 2018
@DanielRosenwasser
Copy link
Member

This could be useful for spreading in argument lists, but why wouldn't you use an object literal instead?

function createSegment(/* ... */): { length: number, count: number } {
  /* ... */
}
@brainkim
Copy link
Author

brainkim commented Oct 31, 2018

This could be useful for spreading in argument lists

Actually, I don't see this feature being very useful in argument lists, b/c the variables you name in the destructuring expression implicitly label the elements of the tuple:

function readSegment([length, count]: [number, number]) {
  /* ... */
}

As for why one would use tuples in a return type vs. objects, or really, why one would use tuples over objects generally; sometimes, it's easier to have data structures with positional values rather than named values, such as when one has to write a bunch of these values (for tests, etc). Consider:

const segments = [
  [1,2],
  [2,0],
  [3,1],
  [4,0],
  createSegment(),
];

vs.

const segments = [
  { length: 1, count: 2 },
  { length: 2, count: 0 },
  { length: 3, count: 1 },
  { length: 4, count: 0 },
  createSegment(),
];

Using tuples gets rid of a lot of repetition/noise.

Additionally, in recent news, the React team is proposing a new API called hooks which is based on returning/destructuring tuples.

Using named tuples as a return value:

function useState<T>(initial: T): [value: T, setter: (T) => void] {
  /* ... */
}

seems much more descriptive than using unnamed tuples:

function useState<T>(initial: T): [T, (T) => void] {
  /* ... */
}
@weswigham weswigham added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Oct 31, 2018
@DanielRosenwasser
Copy link
Member

Actually, I don't see this feature being very useful in argument lists, b/c the variables you name in the destructuring expression implicitly label the elements of the tuple

I mean more in terms of when you indirectly spread a tuple type into a parameter list. It's pretty niche though.

const reverseApply =
  <T extends unknown[]>(...args: T) =>
    <R>(func: (...args: T) => R) =>
        func(...args);
@sindresorhus
Copy link

Named tuple elements would improve code readability. I currently have quality?: [number, number];, which would be much more readable as quality?: [min: number, max: number];.

@streamich
Copy link

I've run into this when I have to define function parameters as a tuple, now I do

type MyFunction = SpecialFunction<[number, number]>;

Then when I use MyFunction function autosuggestion shows me that "arg 0" is of type number. But it would be great if instead of "arg 0" there would be an actual name.

For example named tuple could solve this:

type MyFunction = SpecialFunction<[step: number, iterations: number]>;
@MartinJohns MartinJohns mentioned this issue Dec 4, 2019
5 tasks
@kourge
Copy link

kourge commented Dec 5, 2019

It's worth mentioning that this feature already exists in TypeScript today, but it does not have syntax of its own:

type MyFunction = SpecialFunction<Parameters<(step: number, iterations: number) => 0>>;

Written this way, the arguments of MyFunction retain their names.

@martinheidegger
Copy link

martinheidegger commented Mar 25, 2020

Adding to this: I think it might be nice to have human readable label fallbacks, i.e.:

type timeout = number
type callback = () => void
type parameters = [timeout, callback]

function fn (...rest: parameters): void { }

fn(// -> code completion here for a timeout named parameter, and callback named parameter
@ikokostya
Copy link
Contributor

ikokostya commented Apr 29, 2020

Proposed feature doesn't improve readability in call sites. For example

// What does x (or y) means?
const [x, y] = createSegment();

This is obvious if use interface instead of tuple:

const {length, count} = createSegment();

Rule is very straightforward: if meaning of elements is obvious, then use tuple. Otherwise, use interface. Similar to positional and named arguments. I'm afraid that with new syntax people will start to use tuple in unsuitable places.

As for why one would use tuples in a return type vs. objects, or really, why one would use tuples over objects generally; sometimes, it's easier to have data > structures with positional values rather than named values, such as when one has to write a bunch of these values (for tests, etc). Consider:

const segments = [
  [1,2],
  [2,0],
  [3,1],
  [4,0],
  createSegment(),
];

vs.

const segments = [
  { length: 1, count: 2 },
  { length: 2, count: 0 },
  { length: 3, count: 1 },
  { length: 4, count: 0 },
  createSegment(),
];

The provided example is synthetic. How proposed feature should improve readability in such case? Also, you can always write helper function to reduce repetition.

Moreover, the proposed feature doesn't protect from errors at compile time when order of elements in tuple is changed:

- function createSegment(): [length: number, count: number] {}
+ function createSegment(): [count: number, length: number] {}
@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented May 1, 2020

It sounds like from our discussion, tuple element names won't enforce anything in the type system - they're purely intended to communicate intent. We're going to defer to lint rules to enforce these mistakes instead.

We do have some questions from users about syntax on optional and rest elements. You can vote by clicking each of the respective options

First, we want to know if an optional element should be indicated by a question mark (?) following the name, or following the type:


Next, we want to know whether a rest element should be indicated by a ... immediately preceding the name or immediately preceding the type.


@brainkim
Copy link
Author

brainkim commented May 1, 2020

I personally prefer the first option for both choices, the reason being you could then copy a function parameter list directly into a tuple and have everything work:

function foo(a: string, b?: number, ...c: unknown[]) {
}
// copy and pasted directly below:
type FooArgs = [a: string, b?: number, ...c: unknown[]];
@mohsen1
Copy link
Contributor

mohsen1 commented May 1, 2020

I love this! Two edge cases that came to my mind:

Can you mix labeled and unlabeled?

type range = [start: number, end: number, inclusiveStart: boolean, inclusiveEnd: boolean];
type inclusiveness = [inclusiveStart: boolean, inclusiveEnd: boolean]

type myrange = [number, number, ...rest: inclusiveness]
type anotherone = [a: number, string, c: boolean]

What happens if types are recursive?

type  A = [a: boolean, b: boolean, ...c: A]

@weswigham
Copy link
Member

weswigham commented May 8, 2020

@mohsen1

type  A = [boolean, boolean, ...A];

reports A rest element must be an array type on ...A, the named version works the same way.

Can you mix labeled and unlabeled?

Nope. If you're mix labeled and unlabeled elements you get an error.

@millsp
Copy link
Contributor

millsp commented May 17, 2020

Naming, Moving, and Renaming labels

It would be great if the label could follow the item it has been attached to, even after performing operations on it. Let's take the example of function parameters which already have some kind of labelling system.

Let's say we have a function of type Funct0:

type Funct0 = (a: 1, b: 2, c: 3) => boolean

We extract its type parameters out of it:

type Params = Parameters<Funct0>
// you don't see it here but `Pararms` are labelled

So you'll notice that if we reapply this as-is, names are kept:

type Funct1 = (...args: Params) => boolean
// parameter names were completely preserved

But if we start moving things around, names disappear:

type Funct2 = (...args: [Params[2], Params[1], Params[0]]) => boolean
// now we are left with arguments like: `arg_0, arg_1, arg_2`

So it would be nice if the name (label) could follow around:

type Funct2 = (...args: [Params[2], Params[1], Params[0]]) => boolean
// this way, we could have our original names preserved: `c, b, a`

And similarly, it would be great to be able to rename a label:

type Funct3 = (...args: [x: Params[2], y: Params[1], z: Params[0]]) => boolean
// this way we could override the `c, b, a` into `x, y, z`

So similarly, tuples could benefit from what I've described above: naming a label, moving a label, and renaming a label:

// naming a tuple with labels
type tuple0 = [a: 'a', b: 'b', c: 'c']

// moving labels from a tuple to another
type tuple1 = [tuple[0], tuple[1], tuple[2]]
// labels of `tuple0` were ported to `tuple1`

// renaming labels when moving types
type tuple2 = [x: tuple[0], y: tuple[1], z: tuple[2]]

A great use-case that I see benefits for is when we alter the nature of functions and the order of their arguments. And portable labels (moving labels) would add a huge dose of clarity to the code base too, like shown above.

Thanks @weswigham for your PR, I just tested it. Would you be able to easily integrate this label portability? This would especially benefit developers using curry on ramda and eradicate arg_0 on functions.

I guess my request would imply that some fields can be left label-less, which would require to apply names like arg_x when no label is found.

@vitaly-t
Copy link

vitaly-t commented Aug 1, 2020

This feature feels not quite finished, if we cannot use the tuple names at all...

function test1(a: [first: string, second: string]) {
    const firstValue = a.first; // = a[0]
    const secondValue = a.second; // = a[1]
}

I could understand, if specifically for tuples the names were treated as indexes, in which case the following should work:

function test2(a: [first: string, second: string]) {
    // first = 0, and is of type number
    // second = 1, and is of type number

    const firstValue = a[first]; // = a[0]
    const secondValue = a[second]; // = a[1]
}

But none of these work, unfortunately.

The idea of adding names, is first of all to let you access values through them, and not just for code decorations.

The TypeScript could easily replace names with indexes during transpile time.

@remcohuijser
Copy link

I have to agree with @vitaly-t.

To me, it would make a lot of sense in the context of vector math to perform operations that read like you are working on an object (vector.x) instead of working with arrays (vector[0]). The big plus to me would be readability.

type Vector3 = [x: number, y: number, z: number];

function addInPlace(vector: Vector3, value: number) {
    vector.x += value;
    vector.y += value;
    vector.z += value;
}
@vitaly-t
Copy link

vitaly-t commented Aug 10, 2020

To me, it would make a lot of sense in the context of vector math to perform operations that read like you are working on an object (vector.x) instead of working with arrays (vector[0]). The big plus to me would be readability.

My sentiment exactly! To avoid working with indexes, and instead work with names, makes it so much easier to read such code.

One has to wonder, from all the down-votes, and without explanations, where all the negativity comes from. Seems like just very unfriendly place.

@remcohuijser
Copy link

To give a bit of feedback @vitaly-t (hope you are open to it). In your post, you call the solution half-baked and unfinished which came across to me as a bit judgemental/unfriendly initially 😎.

I guess the solution does solve the problem of making tuple more readable. We are just trying to extend the idea. Let's see what others think!

@remcohuijser
Copy link

remcohuijser commented Aug 10, 2020

One other case I just thought of where using the labels in code might be useful is refactoring. Let's take this function:

function createSegment(/* ... */): [length: number, count: number] {
  /* ... */
}

Now change the return type to something else:

function createSegment(/* ... */): [whatever: number, count: number] {
  /* ... */
}

I guess in this case all hell breaks loose as you will get no compile errors and have to go through all code by hand to adjust it.

@mohsen1
Copy link
Contributor

mohsen1 commented Aug 10, 2020

@vitaly-t @remcohuijser you seems to forget TypeScript is a type system for JavaScript. The code examples you provided are not valid JavaScript code to begin with. That's why readers are confused with your comments. I understand that in other languages such as Swift you can access tuple members via labels and indices. But in TypeScript a tuple is simply an array. A tuple type is describing an array so you can't use labels (type information) to access tuple values (runtime values).

@vitaly-t
Copy link

vitaly-t commented Aug 10, 2020

@mohsen1 But what does stop TypeScript from simply replacing those names with indexes during transpilation? That was the idea I tried to convey from start. This would not affect JavaScript in any way, but make TypeScript code way more readable.

@remcohuijser Cheers, I rephrased my initial post for more leniency :)

@remcohuijser
Copy link

@mohsen1 you are quite right: I forgot about this. I come from a Haxe background by the way. I have read other people state that TypeScript is just a typing system. This would mean that TS just reads files, checks them, and then you are done.

In practice, there are cases where TS is doing a lot more. One obvious example I can think of is JSX (which is not valid JS) that gets compiled to the createElement function call. Others are compilation to ES5, decorators, or even the compiler API.

To me, this shows that yes, the basis of TS is a typing system, but there are optional compiler behaviors that one can enable.

Getting back to this topic: I can imaging that you can "enable" the rewrite behavior so that labels of tuple elements can be used in code. What do you think?

@vitaly-t I am always a bit hesitant to give feedback on the internet 🤣 Thank you for responding is such a professional way!

@weswigham
Copy link
Member

Right, so, officially, we won't ever do the whole "dotted access for labels get transpiled to numbers" thing, because that's type directed (therefore error prone in the presence of inaccurate types or any), and our philosophy is not to add type directed emit features. (Such features also don't work in Babel, which would be a big problem!) That's the final word on that, and we've said as much in the original issue requesting labels.

A lot of why we added labels is for:

  1. Better documentation in the IDE (labels show up in signature transforms and in completions, and provide a place for doc comments to rest, like with object properties) - this allows, say, function arguments passed as a tuple to some machinery to retain their parameter names and documentation, which makes for a better authoring experience.
  2. More information available for linters to make use of. (Who can add stricter restrictions on label usage/compatability than we're willing to incorporate at present)
@remcohuijser
Copy link

@weswigham I think I have to do some more reading on the design goals of TS and type-directed emits. Thank you for pointing this out.

@mohsen1
Copy link
Contributor

mohsen1 commented Aug 11, 2020

I don't think accessing members with a label via dots will ever work if you want to. Take this as an example:

const tuple: [map: (...args: any[]) => any, forEach: any, filter: any] = [() => {}, 2, 3];
tuple.map((oops) => {
 // what is this `map`? Array.prototype.map or tuple[0]??
})
@camsjams
Copy link

camsjams commented Sep 2, 2020

Thank you for this new feature! I think it's extremely useful when peeking at Tuples and trying to surmise exactly which item is which!

🎉

@techieshark
Copy link

In case anyone lands here and wants the TL;DR version:

In TypeScript 4.0, tuples types can now provide labels.

type Range = [start: number, end: number];

https://devblogs.microsoft.com/typescript/announcing-typescript-4-0/#labeled-tuple-elements

@smashah
Copy link

smashah commented May 17, 2021

Is there any way to reference the labels of the tuple as a type itself?

e.g:

type Range = [start: number, end: number];

type RangeLabels = labelof Range //Valid values would be string "start" and "end"
@RyanCavanaugh
Copy link
Member

@smashah no; these are purely for display purposes (similar to parameter names in function types)

@Neme12
Copy link

Neme12 commented Mar 6, 2024

Right, so, officially, we won't ever do the whole "dotted access for labels get transpiled to numbers" thing, because that's type directed (therefore error prone in the presence of inaccurate types or any), and our philosophy is not to add type directed emit features. (Such features also don't work in Babel, which would be a big problem!) That's the final word on that, and we've said as much in the original issue requesting labels.

A lot of why we added labels is for:

  1. Better documentation in the IDE (labels show up in signature transforms and in completions, and provide a place for doc comments to rest, like with object properties) - this allows, say, function arguments passed as a tuple to some machinery to retain their parameter names and documentation, which makes for a better authoring experience.
  2. More information available for linters to make use of. (Who can add stricter restrictions on label usage/compatability than we're willing to incorporate at present)

This makes sense to me, but at least I think the compiler could provide better error messages when someone tries accessing tuple elements by dotted access, explaining that they have to use an index (or deconstruct the tuple).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript