英文:
How do I assign a type to an array of tuples whose entries may vary between tuples?
问题
You can define the args
parameter of foo
in TypeScript as follows to ensure type consistency within tuples:
function foo<T>(args: [T, T][]): void {
// Your function implementation here
}
To handle the case where the second entry's type should match the generic of the first, you can modify the type definition as:
function foo<T, U extends T>(args: [T, U][]): void {
// Your function implementation here
}
This way, it will raise a compile-time type error if there's a type mismatch within the tuples.
英文:
Say I have function foo(args) {...}
where args
is an array of 2-tuples such that the entries within the tuple are the same type (i.e. [T,T]
), but the entries across tuples may vary 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 type the args
parameter of foo
so that mismatching types within a tuples raises a compile-time type error? For example:
foo([
[1, 3],
["hello", 5], // type error here
[true, true],
[2, 7n] // type error here
])
If it's not possible to show the type error inline, making the whole function call error is also acceptable.
Addendum: Can this be made to work with 2-tuples of type [SomeType<T>, T]
(i.e the second entry's type should match the generic of the first), but T can still vary between tuples [[SomeType<T>, T],[SomeType<U>, U],[SomeType<V>, V]]
?
foo([
[{value: 1}, 3],
[{value: "hello"}, 5], // type error here
[{value: true}, true],
[{value: 2}, 7n] // type error here
])
答案1
得分: 1
我认为你可以通过创建一个row
类型来简单实现这个目标,该类型将接受string
、number
或boolean
的数组。
type Row = string[] | boolean[] | number[]
现在,我们可以将这个类型分配给foo
函数的args
参数。
function foo(args: Row[]): void {
// ...
// ...
// ...
}
有了这个类型定义,如果你将一个不匹配行内元素类型的参数传递给foo
函数,TypeScript会引发错误。
这是播放区的链接。
英文:
I think you can simply achieve this by creating a type for a row
which will accept the array of either string
, number
or boolean
.
type Row = string[] | boolean[] | number[]
And now, we can just assign this type for args
parameter for foo
function.
function foo(args: Row[]): void {
...
...
...
}
With this type definition, if you will provide an argument to foo
where the types of elements with in a row did not match, Typescript will raise an error.
Here is the playground link
.
答案2
得分: 1
以下是翻译好的内容:
为了实现这一点,我们需要使用 generics 来处理数组和 mapped types 来映射数组元素。由于我们知道数组应该是一个长度为两的元组数组,我们将推断元组中第一个项目的泛型参数,并使第二个项目具有相同的类型。为了获取泛型参数的类型,我们需要使用 infer 关键字。请注意,我们需要确切地知道(或者至少具有相似形状的那个)用于使其工作的泛型类型,这在我们的情况下是 Variable
:
const foo = <T extends unknown[][]>(arr: {
[K in keyof T]: T[K] extends unknown[]
? T[K][0] extends Variable<infer Type>
? [Variable<Type>, Type]
: T[K]
: T[K];
}) => {}
它可能看起来就是这样,但是让我们看看以下数组的类型:
const arr = [1, '2', false];
// (string | number | boolean)[]
type Arr = typeof arr;
如你所见,类型不完全与我们在数组中的内容相符。编译器扩展了类型,以确保我们可以更改数组元素。为了让编译器知道数组是只读的,我们需要使用 const assertion:
const arr = [1, '2', false] as const;
// readonly [1, "2", false]
type Arr = typeof arr;
看起来不错,现在,这意味着我们需要将传递给 foo
的数组设为只读,并且由于只读数组是可变数组的超集,如果我们尝试将只读数组传递给可变数组,将会出错:
// false
type Case1 = readonly number[] extends number[] ? true : false;
// true
type Case2 = number[] extends readonly number[] ? true : false;
因此,让我们将 foo
中的所有数组类型更新为只读。请注意,由于我们的数组是二维的,内部数组也将是只读的,数组的约束应该是只读数组的只读数组:
const foo = <T extends readonly (readonly unknown[])[]>(arr: {
[K in keyof T]: T[K] extends readonly unknown[]
? T[K][0] extends Variable<infer Type>
? readonly [Variable<Type>, Type]
: T[K]
: T[K];
}) => {};
测试:
declare const ctx1: Variable<number>;
declare const ctx2: Variable<string>;
declare const ctx3: Variable<boolean>;
declare const ctx4: Variable<number>;
declare const ctx5: Variable<number[]>;
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'], // 出错
] as const);
然而,我们仍然存在一些问题。例如,如果元组中的第一个元素是 Variable<7>
,这意味着第二个参数也应该是 7
,而不是任何数字,如果这是一个问题,我们需要获取 7
的原始类型,即数字。这可以使用我的 type-samurai 开源项目中的 ToPrimitive 实用类型来实现:
type ToPrimitive<T> = 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<T[K]>;
};
更新的函数:
const foo = <T extends readonly (readonly unknown[])[]>(arr: {
[K in keyof T]: T[K] extends readonly unknown[]
? T[K][0] extends Variable<infer Type>
? ToPrimitive<Type> extends infer PrimitiveType
? readonly [Variable<PrimitiveType>, PrimitiveType]
: T[K]
: T[K]
: T[K];
}) => {};
另一个问题是,如果我们当前的 foo
实现中推断的类型是 number[]
,那么我们将不会允许只读数组:
foo([
[ctx5, [4, 5, 6]], // 类型 'readonly [4, 5, 6]' 是 'readonly',无法分配给可变类型 'number[]'
] as const)
修复方法非常简单,我们将检查推断的类型是否是某个数组,然后我们将获取其元素类型,并将 readonly ElemenType[]
写入元组的第二个参数中:
const foo = <T extends readonly (readonly unknown[])[]>(arr: {
[K in keyof T]: T[K] extends readonly unknown[]
? T[K][0] extends Variable<infer Type>
? ToPrimitive<Type> extends infer PrimitiveType
? readonly [
Variable<PrimitiveType>,
PrimitiveType extends Array<infer ArrayItem>
? readonly ArrayItem[]
: PrimitiveType,
]
: T[K]
: T[K]
: T[K];
}) => {};
测试:
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,
<details>
<summary>英文:</summary>
To achieve this we will need to use [generics](https://www.typescriptlang.org/docs/handbook/2/generics.html) for the array and [mapped types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html) to map through the elements of the array. Since we know that the array should be an array of tuples of length two, we are going to infer the generic parameter of the first item in the tuple and make the second one of the same type. To get the type of the generic parameter, we need to use the [infer](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#inferring-within-conditional-types) keyword. Note that we need to know exactly (or at least the one that has a similar shape) which generic type is used to make it work, which is `Variable` in our case:
const foo = <T extends unknown[][]>(arr: {
[K in keyof T]: T[K] extends unknown[]
? T[K][0] extends Variable<infer Type>
? [Variable<Type>, Type]
: T[K]
: T[K];
}) => {}
It may look like it is all, however let's see the type of the following array:
const arr = [1, '2', false];
// (string | number | boolean)[]
type Arr = typeof arr;
As you can see, the type is not exactly what we have in the arr. The compiler widens the type to make sure that we can mutate the array elements. To let the compiler know that the array is read-only we will need to use [const assertion](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions):
const arr = [1, '2', false] as const;
// readonly [1, "2", false]
type Arr = typeof arr;
Looks good, now, this means that we will need to make the array that we pass to the `foo` read-only` and since read-only arrays are the supersets of mutable arrays we will get an error if we try to pass a read-only array to just array:
// false
type Case1 = readonly number[] extends number[] ? true : false;
// true
type Case2 = number[] extends readonly number[] ? true : false;
Thus, let's update all array types in the `foo` to read-only. Note that since our array is two-dimensional, the inner arrays will be also read-only and the constraint for the array should be a read-only array of read-only arrays:
const foo = <T extends readonly (readonly unknown[])[]>(arr: {
[K in keyof T]: T[K] extends readonly unknown[]
? T[K][0] extends Variable<infer Type>
? readonly [Variable<Type>, Type]
: T[K]
: T[K];
}) => {};
Testing:
declare const ctx1: Variable<number>;
declare const ctx2: Variable<string>;
declare const ctx3: Variable<boolean>;
declare const ctx4: Variable<number>;
declare const ctx5: Variable<number[]>;
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<7>` it will mean that the second argument should be also `7`, not any number, and if that's an issue we need to get the primitve of the `7` which is number. This can be achieved using [ToPrimitive](https://github.com/KamilHs/type-samurai/blob/main/src/to-primitive.d.ts?plain=1#L13) utility type from my [type-samurai](https://github.com/KamilHs/type-samurai) open-source project:
type ToPrimitive<T> = 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<T[K]>;
};
Updated function:
const foo = <T extends readonly (readonly unknown[])[]>(arr: {
[K in keyof T]: T[K] extends readonly unknown[]
? T[K][0] extends Variable<infer Type>
? ToPrimitive<Type> extends infer PrimitiveType
? readonly [Variable<PrimitiveType>, PrimitiveType]
: T[K]
: T[K]
: T[K];
}) => {};
Another issue is if the inferred type is `number[]` in our current `foo` implementation we won't let the 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 pretty straightforward, we will check whether the inferred type is some array then we will get its elements type and write `readonly ElemenType[]` as the second argument in the tuples:
const foo = <T extends readonly (readonly unknown[])[]>(arr: {
[K in keyof T]: T[K] extends readonly unknown[]
? T[K][0] extends Variable<infer Type>
? ToPrimitive<Type> extends infer PrimitiveType
? readonly [
Variable<PrimitiveType>,
PrimitiveType extends Array<infer ArrayItem>
? readonly ArrayItem[]
: PrimitiveType,
]
: T[K]
: T[K]
: T[K];
}) => {};
Testing:
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 assertion` everywhere. In the Typescript `5.0` the [const type parameters](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#const-type-parameters) were added, which let's avoid `const assertions`:
const foo = <const T extends readonly unknown[]>(item: T) => item
// readonly [1, 2, 3]
const result = foo([1,2,3])
Unfortunately, we are not able to use them, since we do some manipulation with the argument instead of directly assigning `T` as a type to it:
const foo = <const T extends readonly unknown[]>(item: {[K in keyof T]: T[K]}) => item
// const result: (2 | 1 | 3)[]
const result = foo([1, 2, 3])
In conclusion, for now, the `const assertion` is the only way to make sure that it works as expected.
[Link to playground](https://www.typescriptlang.org/play?ts=5.2.0-beta#code/C4TwDgpgBAaghgJwJZwEYBsIB4AqA+KAXigG8oA3OdAVwgC4ocoBfAbgCh2IAPMAewTAooSIz4AFZAFskwJOWz4ijKD2AQAdgBMAzlB3BkGgObsoUAPz7DSE2agMmazbqgbqU1BAT2r7z972jqrc6tp67ujovm7UUUEqzuFQ1NoQAGa2EFoxqVoZWTnmwUmuqHx8mHAaMeWVENUJTqEueqhIxrbAtR1dTSFhrjognpUxw6PRxaT25uYA2gDSULZQANYQIHzpjAC6jhLSsvKKS7t4HHMsHOwAxnwaBlDpFcq4A61QCA1aD+ggUAAFN84L8NP8Uho1ho+AB3DTzXYASkReEBiAQDBI9iWKw06022z2jjOH2SILBENS0LhCN2s0sjDO8wADLsya54Mg0JgsLZ0t5GOAIHgGeYrDhDkgZHIFLhhQRSnp+YLJNLjgocMKxXMrBS-gD5jqruYuSgMNg1TKTlrIHgADTGk1WjUQW3QJVQACCCAQcBAfI0AoQ3t9-oAkuopKKTbHdV8fgbQ36QJGIFJEU7YwwXbK3cLHXG5vSiyTFiWTWWK9McGcOMwkUQCCQ2Jx8rd0IhoPdHkJbsBuABGBhmnnYfxeBAXdjtzvfKA9p797gAJhHiHNvIMRmM09nXYXDyXA4AzOvuRasHUqho9xAOwfF32BwAWc+b8ceSd3h-zp8LgcAFZ3zHLAJ28VEOH3P8j2fbgADYQMvEgNDgKR6GsHd7SgOBjAw8CEGYad2BePhASNBZl0HbCT12QsoHmZcV2wgByWEBHQLQWLonFlxPbDDFoHjKNfbCAHZhIY5dAOw+YX2wmSoHg3ZJMYgd4OwlC0IwgAiAAJJAdOw3CMJXE9mB49k4D0J8kRuUjyN4gdqKgWj6LU1dWPYhBOO49y+IEhAhP80ThCCiA6KgAB6KLVF9AQoAAC28CAnO4RS5IU7DlNU5cNKgQC2WwmK4oQBLku+dgrJs2C7PYIA)
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论