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 · 3 comments
Open

Syntax for explicit tuple literal type #48052

graphemecluster opened this issue Feb 27, 2022 · 3 comments
Labels
Awaiting More Feedback Suggestion

Comments

@graphemecluster
Copy link

@graphemecluster graphemecluster commented Feb 27, 2022

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

@whzx5byb whzx5byb commented Feb 27, 2022

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 Suggestion labels Feb 28, 2022
@graphemecluster
Copy link
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback Suggestion
Projects
None yet
Development

No branches or pull requests

4 participants