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

Syntax for explicit tuple literal type #48052

Open
graphemecluster opened this issue Feb 27, 2022 · 6 comments
Open

Syntax for explicit tuple literal type #48052

graphemecluster opened this issue Feb 27, 2022 · 6 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@graphemecluster
Copy link
Contributor

Suggestion

Though closely related to #10195 and #16656, I am opening this issue because the proposed syntax, purpose and demonstration are different.

Motivation

Previously I wrote a library that manipulates nested arrays:

import NDArray from "ndarray-methods";

NDArray.buildShape([2, 3], 0) // [[0, 0, 0], [0, 0, 0]]
// Works like Python’s numpy.zeros()

But without tuple its type is being inferred as NDArray<number>, of which NDArray is defined as

type NDArray<T> = (T | NDArray<T>)[]

The Problem

I know there are at least 3 ways to cast it as a tuple:

/* 1 */ NDArray.buildShape([2, 3] as const, 0)            // constant tuple
/* 2 */ NDArray.buildShape([2, 3] as [number, number], 0) // number tuple
/* 3 */ NDArray.buildShape(tuple(2, 3), 0)                // utility function

of which the tuple function is defined as

function tuple<T extends unknown[]>(...args: T) { return args; }

In these 3 cases, the program correctly infers the type of the results as number[][].

But these 3 ways all have their problems:

For 1, it is not necessary to make the numbers constants.

For 2, we will have to write number, many times for higher-dimensional arrays (I know we can use a utility type, but I don’t think it’s desirable).

For 3, there are considerable runtime effects and it might cause serious performance problems, especially when it is transpiled down to something like

function tuple() {
  for (var a = [], i = 0; i < arguments.length; i++) a.push(arguments[i]);
  return a;
}

Also, in a module-based application, this utility function will have to be defined or imported before using, which pollutes the namespace.

Most importantly, for 1 and 2, we are using the as keywords, which is not favorable for TypeScript codes in general (and ESLint gets angry too).

I also know the existence of the Record & Tuple Proposal, but since there will be compatibility problems in the near future, and people might not want to refactor the existing codes, the current tuple-like array will still be used by a significant amount of projects.

Solution

In my opinion, a syntax without as like tuple [2, 3] should be created, or we should at least provide a way to let TypeScript infer a literal array as tuple for a certain function parameter.

Prior Use

The tuple utility function is included in many repositories, and even in the release note:

@whzx5byb
Copy link

Related: #30680 (comment)

A simple workaround:

type Cast<A, B> = A extends B ? A : B;

type Narrowable =
| string
| number
| bigint
| boolean;

type Narrow<A> = Cast<A,
| []
| (A extends Narrowable ? A : never)
| ({ [K in keyof A]: Narrow<A[K]> })
>;

type NDArray<T> = (T | NDArray<T>)[];

type FDArray<T, U> = U extends readonly [unknown, ...infer U] ? FDArray<T[], U> : T;
type MDArray<T, U extends readonly unknown[]> = number extends U["length"] ? NDArray<T> : FDArray<T, U>;

declare function buildShape<A extends readonly number[], T>(array: Narrow<A>, value: T): MDArray<T, A>;

buildShape([2,3], 0);
// ^ number[][]

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Feb 28, 2022
@graphemecluster
Copy link
Contributor Author

graphemecluster commented Mar 6, 2022

@whzx5byb Great, but no luck with this though:

type Cast<A, B> = A extends B ? A : B;
type Narrowable = string | number | bigint | boolean;
type _Narrow<A> = [] | (A extends Narrowable ? A : never) | { [K in keyof A]: _Narrow<A[K]> };
type Narrow<A> = Cast<A, _Narrow<A>>;

declare global {
  interface Array<T> {
    map<A extends unknown[], U>(
      this: Narrow<A>,
      callbackfn: (value: A[number], index: number, array: A) => U,
      thisArg?: any
    ): { [P in keyof A]: U };
  }
}

declare function map<A extends unknown[], U>(
  array: Narrow<A>,
  callbackfn: (value: A[number], index: number, array: A) => U,
  thisArg?: any
): { [P in keyof A]: U };

const foo = map([3, 4, 5], String); // [string, string, string]
const bar = [3, 4, 5].map(String); // string[]

Pinging the author of Narrow @millsp for suggestion.

@JoshuaKGoldberg
Copy link
Contributor

JoshuaKGoldberg commented May 28, 2022

Proposal: maybe a new as tuple cast, as a twist on as const that only changes an array to a tuple and doesn't apply readonly anywhere?

// Proposed:
["hello", 123] as tuple; // Type: ["hello", 123];

// Existing alternatives:
["hello", 123]; // Type: (string, number)[]
["hello", 123] as readonly; // Type: readonly ["hello", 123];
["hello", 123] as [string, number]; // Type: [string, number]
["hello", 123] as ["hello", 123]; // Type: ["hello", 123]

Blatantly copying @chaance's suggestion in https://twitter.com/chancethedev/status/1527695898318458880. 🙌

@nicojs
Copy link

nicojs commented May 28, 2022

We might not want to use the word tuple as we might want to reserve that for JS native tuples as described in #49243 (comment)

@rricard
Copy link

rricard commented May 31, 2022

@JoshuaKGoldberg tuple is indeed going to become a primitive type if Record & Tuple becomes part of ECMA262. Specifically it will be something that can be returned by typeof: https://tc39.es/proposal-record-tuple/#sec-typeof-operator-runtime-semantics-evaluation

@jeremyVignelles
Copy link

Instead of the as tuple, that seems like a good idea at first, but may cause potential compatibility issues, may I suggest the use of as itself?

Like this:

const a = [1, 'a'] as itself // would be of type [1, 'a']

or maybe as literal ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants