-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
suggestion: explicit "tuple" syntax #16656
Comments
Very related to #10195 |
Similar syntax suggestion; different purposes. As a former TypeScript-hater & JavaScript purist, I can tell you the verbosity was a turnoff. Expanding the inference capabilities of TS -- and subsequently featuring them! -- would make it easier to approach & use for many like myself. |
Just FYI, your conservative solution already works if you leave off the word class McMonkey implements McBean {
baz = {
bar: <[number, number]> [0, 1] // no error
};
} but I understand you'd rather put typescript through the Annotation-Off Machine. |
This is valid TS: const a: [string, string] = ['a', 'b', 'c']
const b: string[] = ['a'] as [string, string] It can be dealt with, but requires caution and discipline, which ideally TS should not require for types. Proposal: infer literal types from literal array notation |
@jcalz Right; perhaps then what I should have suggested is a When speaking of annotations, without a doubt, the best types are the types without. |
Perhaps we coud break a portion of the the This breakup of |
@masaeedu https://github.com/Microsoft/TypeScript/blob/master/lib/lib.d.ts#L986 function ro<T>(x: Array<T>): ReadonlyArray<T> {
return x;
}
const j = ro([1, 2, 3]);
j[0] = 10; // Error
j.push(11); // Error |
@RyanCavanaugh Awesome! That means we're almost there, but It'd be nice if the compiler was happy with the following: // Spread args are not allowed to be ReadOnlyArray :(
function ro<T>(...args: ReadOnlyArray<T>) {
return args;
}
// Safe to infer readonly equivalent of [1, 2, 3] tuple type for j!
const j = ro(1, 2, 3); |
You can simulate a built-in
Note that the longer definitions do need to come first. |
Built-in language support for an explicit Aggressive conversion of tuples to arrays is a common problem. See also: #3369, #8276, #15071, #16389, #16503, #16700... more At present, it's hypothetically possible to explicitly annotate a tuple everywhere it would be converted to an array, but that's only feasible if (a) you can/have imported the element type definitions, and (b) they aren't a mess of unioned or anonymous types. |
In that #16389 I argued to have The primary counter-argument found there was granular types would break Am I missing any flaws there? |
@tycho01 that's kind of an awesome idea and it would open up so many scenarios that are valuable but at the same time it would be a massive breaking change. |
@aluanhaddad: perhaps if we know what else might break we could tackle them one by one? If the workarounds could be as simple as adding tuple interfaces to |
For tuples that might be all it would take, and could quite well be worth it at that point but I guess I was thinking of it in a more generalized form that would apply to the top level properties of object literals as well. In general I don't use classes and favor a style making heavy use of standard functions, object literals, arrays, and |
What's that stand for?
You just might find you like it. :)
Oh, definitely, I wasn't intending to constrain ourselves here either. And I think objects never were a problem here. Let's to go over the details then: type x = Partial<[1]>; Methods: length?: number;
toString?: () => string;
toLocaleString?: () => string;
push?: (...items: 1[]) => number;
pop?: () => 1;
concat?: {
(...items: 1[][]): 1[];
(...items: (1 | 1[])[]): 1[];
};
join?: (separator?: string) => string;
reverse?: () => 1[];
shift?: () => 1;
slice?: (start?: number, end?: number) => 1[];
sort?: (compareFn?: (a: 1, b: 1) => number) => [1];
splice?: {
(start: number, deleteCount?: number): 1[];
(start: number, deleteCount: number, ...items: 1[]): 1[];
};
unshift?: (...items: 1[]) => number;
indexOf?: (searchElement: 1, fromIndex?: number) => number;
lastIndexOf?: (searchElement: 1, fromIndex?: number) => number;
every?: {
(callbackfn: (this: void, value: 1, index: number, array: 1[]) => boolean): boolean;
(callbackfn: (this: void, value: 1, index: number, array: 1[]) => boolean, thisArg: undefined): boolean;
<Z>(callbackfn: (this: Z, value: 1, index: number, array: 1[]) => boolean, thisArg: Z): boolean;
};
some?: {
(callbackfn: (this: void, value: 1, index: number, array: 1[]) => boolean): boolean;
(callbackfn: (this: void, value: 1, index: number, array: 1[]) => boolean, thisArg: undefined): boolean;
<Z>(callbackfn: (this: Z, value: 1, index: number, array: 1[]) => boolean, thisArg: Z): boolean;
};
forEach?: {
(callbackfn: (this: void, value: 1, index: number, array: 1[]) => void): void;
(callbackfn: (this: void, value: 1, index: number, array: 1[]) => void, thisArg: undefined): void;
<Z>(callbackfn: (this: Z, value: 1, index: number, array: 1[]) => void, thisArg: Z): void;
};
map?: {
<U>(this: [1, 1, 1, 1, 1], callbackfn: (this: void, value: 1, index: number, array: 1[]) => U): [U, U, U, U, U];
<U>(this: [1, 1, 1, 1, 1], callbackfn: (this: void, value: 1, index: number, array: 1[]) => U, thisArg: undefined): [U, U, U, U, U];
<Z, U>(this: [1, 1, 1, 1, 1], callbackfn: (this: Z, value: 1, index: number, array: 1[]) => U, thisArg: Z): [U, U, U, U, U];
<U>(this: [1, 1, 1, 1], callbackfn: (this: void, value: 1, index: number, array: 1[]) => U): [U, U, U, U];
<U>(this: [1, 1, 1, 1], callbackfn: (this: void, value: 1, index: number, array: 1[]) => U, thisArg: undefined): [U, U, U, U];
<Z, U>(this: [1, 1, 1, 1], callbackfn: (this: Z, value: 1, index: number, array: 1[]) => U, thisArg: Z): [U, U, U, U];
<U>(this: [1, 1, 1], callbackfn: (this: void, value: 1, index: number, array: 1[]) => U): [U, U, U];
<U>(this: [1, 1, 1], callbackfn: (this: void, value: 1, index: number, array: 1[]) => U, thisArg: undefined): [U, U, U];
<Z, U>(this: [1, 1, 1], callbackfn: (this: Z, value: 1, index: number, array: 1[]) => U, thisArg: Z): [U, U, U];
<U>(this: [1, 1], callbackfn: (this: void, value: 1, index: number, array: 1[]) => U): [U, U];
<U>(this: [1, 1], callbackfn: (this: void, value: 1, index: number, array: 1[]) => U, thisArg: undefined): [U, U];
<Z, U>(this: [1, 1], callbackfn: (this: Z, value: 1, index: number, array: 1[]) => U, thisArg: Z): [U, U];
<U>(callbackfn: (this: void, value: 1, index: number, array: 1[]) => U): U[];
<U>(callbackfn: (this: void, value: 1, index: number, array: 1[]) => U, thisArg: undefined): U[];
<Z, U>(callbackfn: (this: Z, value: 1, index: number, array: 1[]) => U, thisArg: Z): U[];
};
filter?: {
(callbackfn: (this: void, value: 1, index: number, array: 1[]) => any): 1[];
(callbackfn: (this: void, value: 1, index: number, array: 1[]) => any, thisArg: undefined): 1[];
<Z>(callbackfn: (this: Z, value: 1, index: number, array: 1[]) => any, thisArg: Z): 1[];
};
reduce?: {
(callbackfn: (previousValue: 1, currentValue: 1, currentIndex: number, array: 1[]) => 1, initialValue?: 1): 1;
<U>(callbackfn: (previousValue: U, currentValue: 1, currentIndex: number, array: 1[]) => U, initialValue: U): U;
};
reduceRight?: {
(callbackfn: (previousValue: 1, currentValue: 1, currentIndex: number, array: 1[]) => 1, initialValue?: 1): 1;
<U>(callbackfn: (previousValue: U, currentValue: 1, currentIndex: number, array: 1[]) => U, initialValue: U): U;
};
find?: {
(predicate: (this: void, value: 1, index: number, obj: 1[]) => boolean): 1;
(predicate: (this: void, value: 1, index: number, obj: 1[]) => boolean, thisArg: undefined): 1;
<Z>(predicate: (this: Z, value: 1, index: number, obj: 1[]) => boolean, thisArg: Z): 1;
};
findIndex?: {
(predicate: (this: void, value: 1, index: number, obj: 1[]) => boolean): number;
(predicate: (this: void, value: 1, index: number, obj: 1[]) => boolean, thisArg: undefined): number;
<Z>(predicate: (this: Z, value: 1, index: number, obj: 1[]) => boolean, thisArg: Z): number;
};
fill?: (value: 1, start?: number, end?: number) => [1];
copyWithin?: (target: number, start: number, end?: number) => [1];
entries?: () => IterableIterator<[number, 1]>;
keys?: () => IterableIterator<number>;
values?: () => IterableIterator<1>; So I guess the question is where we now ended up with
The LHS of function params is co-variant, e.g. for declare function sort(compareFn?: (a: 1, b: 1) => number): [1];
declare function myComparer(a: number, b: number): number;
let c = sort(myComparer);
// ^ passes, 1 within LHS of function param `compareFn` is covariant
Actual contra-variant positions appear in:
Methods marked with * incidentally also mutate their data, which sucks to reflect through static types especially of tuples, though technically that was a thing for homogeneous arrays as well: const tpl1 = [90210]; // number[]
tpl1.push('Beverly Hills');
// boom
const tpl2: [90210] = [90210];
tpl2.push('Beverly Hills');
// boom Imagined solution for unary tuple is as follows. Binary means replace interface Tuple1<T1> {
push<U>(...items: U[]): number;
concat<U>(...items: U[][]): Array<T1 | U>;
concat<U>(...items: (U | U[])[]): Array<T1 | U>;
splice(start: number, deleteCount?: number): Array<T1>;
splice<U>(start: number, deleteCount: number, ...items: U[]): Array<T1 | U>;
unshift(...items: any[]): number;
indexOf(searchElement: any, fromIndex?: number): number;
lastIndexOf(searchElement: any, fromIndex?: number): number;
fill<U>(value: U, start?: number, end?: number): Array<T1 | U>;
} Additional considerations:
Solution so as not to get in the way of all those people using custom prototype methods on |
@tycho01 thank you for the detailed response, and in particular for explaining how it relates to my use cases. Ramda I shall try! CFA stands for Control Flow Analysis. |
Trying that compiler flag at https://github.com/tycho01/TypeScript/tree/16656-granularConst. Haven't fully figured it out yet though. |
Progress: PR at #17785. still testing more though. |
So while creating a related suggestion (#22679) I stumbled upon a way to accomplish this in current versions of typescript that I haven't seen before. Fair warning though that it is just taking advantage of an implementation detail so it might stop working in the future. function tuple<T extends any[] & {"0": any}>(array: T): T { return array }
declare function needsTuple(arg: [string, number]): void
const regularArray = ["str", 10]
needsTuple(regularArray) // error
const myTuple = tuple(["str", 10])
needsTuple(myTuple) // no error or the example from the first post in this issue: function tuple<T extends any[] & {"0": any}>(array: T): T { return array }
interface Foo {
bar: [number, number];
}
interface McBean {
baz: Foo;
}
class McMonkey1 implements McBean {
baz = {
bar: [0, 1]
};
}
class McMonkey2 implements McBean {
baz = {
bar: tuple([0, 1])
};
} |
The proposed "radical solution" would mean that in certain circumstances TypeScript is no longer a superset of JavaScript, so it is probably a no-go. However we could maybe steal F#'s array syntax. Note that there is a very common circumstance where I'd like to use this, namely map initializers. Right now I have to write in several places, // in a context where myObjs :: IObject[] and IObject extends {name: string}
const myDict = new Map<string, { seen: boolean, obj: IObject }>();
for (const obj of myObjs) {
myDict.set(obj.name, { seen: false, obj });
}
// do something that might "see" an obj in myDict
// update the ones that were "seen"
// then remove ones that were not "seen" Note that I am not using the array initializer because it makes the code much less readable and possibly might hide type errors with the coercive weight of const myDict = new Map(
myObjs.map(obj => [ obj.name, { seen: false, obj } ] as [ string, { seen: boolean, obj: IObject } ])
); note that there's two sources of noise here, the intrinsic noise of the If we allow const myDict = new Map(
myObjs.map(obj => [| obj.name, { seen: false, obj } |])
) Note that |
Hi. This is going to overlap with @crdrost's point about the map function, but I'll voice my concern here. I am working on a pet project which uses React and Immutable.js. Sometimes Typescript doesn't seem to infer tuples, so I found writing in functional-programming style pretty cumbersome. In the context of React, I often use map and reduce to create updated copies of new states. For example, I would like to use reduce (and map) like this... const arrayOfMeaninglessNumbers = [1, 20, 300, 4000, 50000];
{
const [a, b, c, d] = arrayOfMeaninglessNumbers
.reduce(([a1, b1, c1, d1], value) => [a1, b1, c1 + value, d1], [new Map(), [], 0, "hello"]);
} The compiler rejects it because c1's type is a union of every values' types instead of number. To get around it, I need to explicitly type the tuples twice, like so... type FancyTuple = [Map<string, number>, string[], number, string]; // usually this is inlined manually
{
// explicitly annotate the tuple without coercion
const base: FancyTuple = [new Map(), [], 0, "hello"];
const [a, b, c, d] = arrayOfMeaninglessNumbers
.reduce(([a1, b1, c1, d1], value) => {
// explicitly annotate the tuple without coercion
const ret: FancyTuple = [a1, b1, c1 + value, d1];
return ret;
}, base);
} However, I don't want base to live beyond reduce's scope. Alternatively, I use an object as the accumulator, but the left-hand-side seems noisy, due to naming and the linter's no-shadow-variable rule. {
const { a1: a, b1: b, c1: c, d1: d } = arrayOfMeaninglessNumbers
.reduce((acc, value) => ({ ...acc, c1: acc.c1 + value }), {
a1: new Map(),
b1: [],
c1: 0,
d1: "hello",
});
} Unless there's a way to explicitly mark tuples, it is very tempting to write imperatively with for-of loops and forgo immutability, especially when partitioning data, which often need intermediate results to be carried over for later uses. |
@achankf while we're waiting for this we may want to use tvald's workaround above, which can fit nicely into a module that you can import where needed. We can also avoid overloading a single function word if that raises hairs... function quad<A, B, C, D>(a: A, b: B, c: C, d: D): [A, B, C, D] {
return [a, b, c, d];
}
const arrayOfMeaninglessNumbers = [1, 20, 300, 4000, 50000];
{
const [a, b, c, d] = arrayOfMeaninglessNumbers
.reduce(([a1, b1, c1, d1], value) =>quad(a1, b1, c1 + value, d1), quad(new Map(), [], 0, "hello"));
} Similar with |
I believe this is solved or at least sufficiently addressed with |
@RyanCavanaugh unfortunately most libraries exporting pure functions that operate on arrays or tuples are not rigorous about declaring their inputs export function first<T>(xs: T[]): T {
return xs[0];
} so const xs = [1,2,3] as const;
first(xs);
// Argument of type 'readonly [1, 2, 3]' is not assignable to parameter of type 'unknown[]'.
// The type 'readonly [1, 2, 3]' is 'readonly' and cannot be assigned to the mutable type 'unknown[]'.ts(2345) So I've had to keep this utility around: export const tuple = <A extends any[]>(...elements: A): A => elements; |
@RyanCavanaugh Possibly: Allow |
@RyanCavanaugh I regularly use functional approaches, e.g. when mapping/filtering objects with declare function test(arr: unknown[]): void;
const values = [1, 2, 3] as const;
test(values );
// ^ 'readonly' and cannot be assigned to the mutable type @rauschma's suggestion is one I've frequently wished for, or an alternative to function example(a: string, b: number, c: boolean) {
const values = [1, 2, 3] as exact;
// [1, 2, 3]
const values2 = [a, b, c] as exact;
// [string, number, boolean]
} |
@csantos42 I'm in that exact same situation, and would also love an |
I would really love this feature. I always thought |
The originally posed problem can be somewhat addressed by the new interface Foo {
bar: [number, number];
}
interface McBean {
baz: Foo;
}
class McMonkey implements McBean {
baz = {
bar: [0, 1] satisfies [number, number],
};
} |
Problem
I'm writing this after encountering (what I think is) #3369. I'm a noob to TS, so I apologize for any misunderstandings on my part. The behavior the lack of type inference here:
Because array literals are (correctly) inferred to be arrays, TS is limited in its ability to infer "tuple". This means there's an added overhead to working with tuples, and discourages use.
As I see it, the problem is that a tuple is defined using array literal notation.
A Conservative Solution
Adding a type query (?) such as
tuple
(.e.gtuple
) would be better than nothing:...but this is still clunky, because you'd have to use it just as much as you'd have to explicitly declare the type.
A Radical Solution
There's a precedent (Python) for a tuple syntax of
(x, y)
. Use it:Obvious Problem
The comma operator is a thing, so
const oops = 0, 1
is valid JavaScript.0
is just a noop, and the value ofoops
will be1
. Python does not have a comma operator (which is meaningful in itself).I've occasionally used the comma operator in a
for
loop, similar to the MDN article. Declaringvar
's at the top of a function is/was common:Parens are of course used in the syntax of loops and conditionals, as well as a means of grouping expressions.
A nuance of Python's tuples are that they must contain at one or more commas, which could help:
...or it could actually make matters worse, because
(0, )
is an unterminated statement in JavaScript.That all being said, using the suggested Python-like syntax, it seems difficult but possible for the compiler to understand when the code means "tuple" vs. when it doesn't.
I'd be behind any other idea that'd achieve the same end. I don't know how far TS is willing to diverge from JS, and I imagine significant diversions such as the above are not taken lightly.
(I apologize if something like this has been proposed before; I searched suggestions but didn't find what I was looking for)
The text was updated successfully, but these errors were encountered: