How to assign a type to an array of tuples whose entries may vary from tuple to tuple?
P粉068486220
P粉068486220 2023-09-06 22:44:17
0
2
403

Suppose I have function foo(args) {...} where args is an array of tuples such that the entries in the tuple are of the same type (i.e. [T,T]), but entries across tuples may change arbitrarily (i.e. [[T,T], [U,U ],[V,V]]). For example:

foo([
  [1, 3],
  ["hello", "world"],
  [true, true],
  [2, 7]
]) // no error

How should I input the args arguments of foo so that mismatched types in the tuple throw a compile-time type error? For example:

foo([
  [1, 3],
  ["hello", 5], // type error here
  [true, true],
  [2, 7n] // type error here
])

If the type error cannot be shown inline, it is also acceptable to make the entire function call wrong.


Appendix: Is it possible to use a 2-tuple of type [SomeType, T] (i.e. the second entry should be of the same type as the first), but T Can still vary between tuples [[SomeType, T],[SomeType, U],[SomeType, V]]?

foo([
  [{value: 1}, 3],
  [{value: "hello"}, 5], // type error here
  [{value: true}, true],
  [{value: 2}, 7n] // type error here
])

P粉068486220
P粉068486220

reply all(2)
P粉948258958

I think you can achieve this simply by creating a type for row that will accept a string, number, or boolean value.

Type Row = string[] |Boolean value[]|Number[]

Now we can assign this type to the args arguments of the foo function.

function foo(args: Row[]): void {
 ...
 ...
 ...
}

With this type definition, Typescript will throw an error if you provide an argument to foo where the types of the elements in the row do not match.

This is the playgroundLink代码>.

P粉136356287

In order to achieve this, we need to use Generic array and mapping type to map the elements of the array. Since we know that the array should be an array of tuples of length 2, we will infer the generic parameter for the first item in the tuple and make the second item have the same type. To get the type of a generic parameter, we need to infer the keyword using the . Note that we need to know exactly (or at least a similarly shaped type) the generic type used to make it work, which in our case is Variable:

const foo = (arr: {
  [K in keyof T]: T[K] extends unknown[]
    ? T[K][0] extends Variable
      ? [Variable, Type]
      : T[K]
    : T[K];
  }) => {}

It may seem like that’s all, but let’s look at the types of the following arrays:

const arr = [1, '2', false];
// (string | number | boolean)[]
type Arr = typeof arr;

As you can see, the type is not exactly the same as what we have in arr. The compiler extends the type to ensure that we can change array elements. To let the compiler know that the array is read-only, we need to use the const assertion:

const arr = [1, '2', false] as const;
// readonly [1, "2", false]
type Arr = typeof arr;

Looks good now, this means we need to set the array passed to foo to read-only`, and since read-only arrays are a superset of the mutable arrays we would get if we tried Passing a read-only array to an array gives an error:

// false
type Case1 = readonly number[] extends number[] ? true : false;
// true
type Case2 = number[] extends readonly number[] ? true : false;

Therefore, we update all array types in foo to be read-only. Note that since our array is two-dimensional, the inner array will also be read-only, and the array's constraints should be read-only arrays of read-only arrays:

const foo = (arr: {
  [K in keyof T]: T[K] extends readonly unknown[]
    ? T[K][0] extends Variable
      ? readonly [Variable, Type]
      : T[K]
    : T[K];
}) => {};

test:

declare const ctx1: Variable;
declare const ctx2: Variable;
declare const ctx3: Variable;
declare const ctx4: Variable;
declare const ctx5: Variable;
declare const ctx6: Variable<{ name: string; age: number }>;

foo([
  [ctx1, 3],
  [ctx2, 'world'],
  [ctx3, true],
  [ctx4, 7],
] as const);

foo([
  [ctx1, 3],
  [ctx2, 'world'],
  [ctx3, true],
  [ctx4, 'invalid'], // error
] as const);

However, we still have some problems. For example, if the first element in the tuple is Variable, it means that the second parameter should also be 7, not any number, if this is a The problem is that we need to get the primitive of 7, which is a number. This can be done using the ToPrimitive utility type from my type-samurai open source project: p>

type ToPrimitive = T extends string
  ? string
  : T extends number
  ? number
  : T extends null
  ? null
  : T extends undefined
  ? undefined
  : T extends boolean
  ? boolean
  : T extends bigint
  ? bigint
  : T extends symbol
  ? symbol
  : {
      [K in keyof T]: ToPrimitive;
    };

Update function:

const foo = (arr: {
  [K in keyof T]: T[K] extends readonly unknown[]
    ? T[K][0] extends Variable
      ? ToPrimitive extends infer PrimitiveType
        ? readonly [Variable, PrimitiveType]
        : T[K]
      : T[K]
    : T[K];
}) => {};

Another problem is that if the inferred type in our current foo implementation is number[], we will not allow read-only arrays:

foo([
  [ctx5, [4, 5, 6]], // The type 'readonly [4, 5, 6]' is 'readonly' and cannot be assigned to the mutable type 'number[]'
] as const)

The fix is ​​very simple, we will check if the inferred type is some array, then get its element type and pass readonly ElementType[] as the second parameter in the tuple:

const foo = (arr: {
  [K in keyof T]: T[K] extends readonly unknown[]
    ? T[K][0] extends Variable
      ? ToPrimitive extends infer PrimitiveType
        ? readonly [
            Variable,
            PrimitiveType extends Array
              ? readonly ArrayItem[]
              : PrimitiveType,
          ]
        : T[K]
      : T[K]
    : T[K];
}) => {};

test:

foo([
  [ctx1, 3],
  [ctx2, 'world'],
  [ctx3, true],
  [ctx4, 7],
  [ctx5, [4, 5, 6]],
  [ctx6, {name: "Hi", age: 23}],
] as const);

foo([
  [ctx1, 3],
  [ctx2, 'world'],
  [ctx3, true],
  [ctx4, true], // error here
  [ctx5, [4, 5, 6]],
  [ctx6, 50], // error here
] as const);

The annoying part is that we need to use const assertions everywhere. In Typescript 5.0, const type parameters so we can avoid const assertions:

const foo = (item: T) => item
// readonly [1, 2, 3] 
const result = foo([1,2,3])

Unfortunately we can't use them because we do something with the parameter instead of directly assigning T as a type:

const foo = (item: {[K in keyof T]: T[K]}) => item

// const result: (2 | 1 | 3)[]
const result = foo([1, 2, 3])

In short, currently, const assertions are the only way to ensure that it works as expected.

Link to Playground

Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
About us Disclaimer Sitemap
php.cn:Public welfare online PHP training,Help PHP learners grow quickly!