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

suggestion: explicit "tuple" syntax #16656

Open
boneskull opened this issue Jun 20, 2017 · 29 comments
Open

suggestion: explicit "tuple" syntax #16656

boneskull opened this issue Jun 20, 2017 · 29 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@boneskull
Copy link
Contributor

boneskull commented Jun 20, 2017

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:

interface Foo {
  bar: [number, number];
}

interface McBean {
  baz: Foo;
}

// error: McMonkey does not implement McBean
class McMonkey implements McBean {
  baz = {
    bar: [0, 1]
  };
}

// vs

// no error
class McMonkey implements McBean {
  baz: Foo = {
    bar: [0, 1]
  };
}

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.g tuple) would be better than nothing:

class McMonkey implements McBean {
  baz = {
    bar: <tuple [number,number]> [0, 1]
  };
}

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

interface Foo {
  bar: (number, number);
}

interface McBean {
  baz: Foo;
}

class McMonkey implements McBean {
  baz = {
    bar: (0, 1)
  };
}

Obvious Problem

The comma operator is a thing, so const oops = 0, 1 is valid JavaScript. 0 is just a noop, and the value of oops will be 1. 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. Declaring var's at the top of a function is/was common:

function () {
  var a, b, c;
}

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:

foo = (0) # syntax error or just `0`; can't recall
foo = (0,) # valid tuple

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

@DanielRosenwasser
Copy link
Member

Very related to #10195

@DanielRosenwasser DanielRosenwasser added the Suggestion An idea for TypeScript label Jun 20, 2017
@boneskull
Copy link
Contributor Author

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.

@jcalz
Copy link
Contributor

jcalz commented Jun 20, 2017

Just FYI, your conservative solution already works if you leave off the word tuple:

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.

@arturkulig
Copy link

arturkulig commented Jun 21, 2017

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

@boneskull
Copy link
Contributor Author

@jcalz Right; perhaps then what I should have suggested is a tuple built-in type, like object.

When speaking of annotations, without a doubt, the best types are the types without.

@masaeedu
Copy link
Contributor

masaeedu commented Jun 27, 2017

Perhaps we coud break a portion of the the Array interface out into a ReadOnlyArray interface, which Array would extend, and provide some syntax (I have no preference here, as an example perhaps `["foo", "bar"] or <readonly>["foo", "bar"]), to create ReadOnlyArray literals. Since the elements of a ReadOnlyArray are not mutable positions (by virtue of the limited API it exposes), they may safely be inferred as literals.

This breakup of Array would be necessary anyway for work on co/contravariance.

@RyanCavanaugh
Copy link
Member

@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

@masaeedu
Copy link
Contributor

masaeedu commented Jun 27, 2017

@RyanCavanaugh Awesome! That means we're almost there, but j is still not being inferred as [1, 2, 3]. That's probably because the expression [1, 2, 3] widens to number[] and irrevocably throws away the type information.

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

@tvald
Copy link

tvald commented Jul 13, 2017

You can simulate a built-in Tuple type via the following:

function Tuple<T1, T2, T3, T4, T5>(t: [T1, T2, T3, T4, T5]): [T1, T2, T3, T4, T5]
function Tuple<T1, T2, T3, T4>(t: [T1, T2, T3, T4]): [T1, T2, T3, T4]
function Tuple<T1, T2, T3>(t: [T1, T2, T3]): [T1, T2, T3]
function Tuple<T1, T2>(t: [T1, T2]): [T1, T2]
function Tuple<T1>(t: [T1]): [T1]
function Tuple(t: any): any { return t }

const x = Tuple([1, '', {}, 1]) // [number, string, object, number]

Note that the longer definitions do need to come first.

@tvald
Copy link

tvald commented Jul 13, 2017

Built-in language support for an explicit Tuple (or tuple) type would be highly desirable.

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.

@KiaraGrouwstra
Copy link
Contributor

In that #16389 I argued to have const infer const x = [1, '', {}, 1] to be of type [1, '', {}, 1] (as opposed to [number, string, object, number] mentioned in the Tuple suggestion above), and ditto for objects, mirroring how const already infers literals for primitives.
Retaining all info this way matters for e.g. Ramda's path function, which would use the literals to navigate a given structure.

The primary counter-argument found there was granular types would break Array.prototype.push, which I believe could be fixed with a push overload on a tuple interface in lib.d.ts.

Am I missing any flaws there?

@aluanhaddad
Copy link
Contributor

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

@KiaraGrouwstra
Copy link
Contributor

@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 lib.d.ts, then great.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Aug 6, 2017

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 {...x}. A big pain point is not getting literal type inference on any object literal properties. Since I don't reassign them I would like them to be literal types so that I can take advantage of CFA (especially exhaustiveness checking) and improved type-inference. I probably just need to use more helper libraries like Ramda 😁

@KiaraGrouwstra
Copy link
Contributor

@aluanhaddad:

CFA

What's that stand for?

I probably just need to use more helper libraries like Ramda 😁

You just might find you like it. :)
I can't say its type inference is perfect yet, but we've tried to make it usable. Feel free to give it a go and give feedback on that if you run into issues.

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.

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 1 in a contra-variant positions within these methods. This means:

  • LHS position
  • conversely, RHS of function params. On Array prototype this occurs only for reduce / reduceRight, which fortunately also have an overload with U generic without these requirements.

The LHS of function params is co-variant, e.g. for sort's definition sort?: (compareFn?: (a: 1, b: 1) => number) => [1];:

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

Array methods where it only ends up in such fake LHS positions include sort, every, some, foreach, map, filter, find, findIndex.

Actual contra-variant positions appear in:

  • push*
  • concat
  • splice*
  • unshift*
  • indexOf
  • lastIndexOf
  • fill*
  • reduce / reduceRight (don't really count as they have an overload where it's only in fake LHS)

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 T1 with T1 | T2, etc.

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:

  • Object: the Object interface is not parameterized, so hard to screw up the couple prototype methods it has. So yeah, objects should be safe either way @aluanhaddad. :)
  • classes: unaffected.
  • custom prototype methods on Array with T in contra-variant positions then instantiating a literal array using const: affected. Recommended workaround would be using let over const there. An alternative would be adding unaffected overloads.

Solution so as not to get in the way of all those people using custom prototype methods on Array with T in contra-variant positions then instantiating a literal array using const: put the proposed change behind a compiler flag.

@aluanhaddad
Copy link
Contributor

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

@KiaraGrouwstra
Copy link
Contributor

Trying that compiler flag at https://github.com/tycho01/TypeScript/tree/16656-granularConst. Haven't fully figured it out yet though.

@KiaraGrouwstra
Copy link
Contributor

Progress: PR at #17785. still testing more though.

@kpdonn
Copy link
Contributor

kpdonn commented Mar 18, 2018

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

Playground link

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

Playground Link

@crdrost
Copy link

crdrost commented Jun 8, 2018

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 as:

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 map and the unnecessary noise of telling TypeScript that I meant to write a [string, object] tuple not a (string | object)[] array, and either one of them is readable on its own (the type declaration is no more complex than the type above) but it's that they have to be joined together which makes this look jarring. So the above keeps the type declaration in exchange for a loop.

If we allow [| ... |] for a tuple constructor, we could do the reverse:

const myDict = new Map(
  myObjs.map(obj => [| obj.name, { seen: false, obj } |])
)

Note that | being a binary operator cannot legally appear directly before ] or after [ so it should preserve the superset-ed-ness?

@RyanCavanaugh RyanCavanaugh added the In Discussion Not yet reached consensus label Jun 8, 2018
@achankf
Copy link

achankf commented Jun 22, 2018

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.

@crdrost
Copy link

crdrost commented Jun 25, 2018

@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 pair<A, B>(a: A, b: B) and triple<A, B, C>(a: A, b: B, c: C).

@RyanCavanaugh
Copy link
Member

I believe this is solved or at least sufficiently addressed with as const. Thoughts?

@pelotom
Copy link

pelotom commented Aug 19, 2019

@RyanCavanaugh unfortunately most libraries exporting pure functions that operate on arrays or tuples are not rigorous about declaring their inputs readonly, e.g.

export function first<T>(xs: T[]): T {
  return xs[0];
}

so as const doesn't play well with them:

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;

@rauschma
Copy link

@RyanCavanaugh as const worked well in my case (where I needed to pass on a complicated mapped type).

Possibly: Allow as tuple as an alternative syntax when the operand is an Array. Rationale: Expresses the intent better.

@csvn
Copy link

csvn commented Mar 10, 2020

@RyanCavanaugh I regularly use functional approaches, e.g. when mapping/filtering objects with Object.entries/Object.fromEntries, and I had hoped as const could be to avoid type widening of tuple to array. In many cases I've found though that the readonly array type prevents me from passing it to other functions, and as such often requires to explicitly type out the tuple type:

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 as const that just disables type widening, but does not add readonly modifier:

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

@devinrhode2
Copy link

@csantos42 I'm in that exact same situation, and would also love an as exact assertion. Ideally TS didn't just throw away the type information in these situations.

@gausie
Copy link

gausie commented May 25, 2021

I would really love this feature. I always thought as let might communicate well the difference between a tuple being readonly with as const vs being mutable.

@MichaelMitchell-at
Copy link

The originally posed problem can be somewhat addressed by the new satisfies operator:

interface Foo {
  bar: [number, number];
}

interface McBean {
  baz: Foo;
}

class McMonkey implements McBean {
  baz = {
    bar: [0, 1] satisfies [number, number],
  };
}

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
Projects
None yet
Development

Successfully merging a pull request may close this issue.