网站搜索

TypeScript:某个值类型的对象属性元组的泛型类型


我想知道是否有可能定义一个通用类型,它是一个长度为 2 的元组 [O, K],称之为 AccessorPair,其中第一个类型 O 是一个对象,第二个类型 K 是该对象的键,使得 O[K] 扩展某个特定类型 T。

例如,

const foo: AccessorPair<number> = [new Array(), 'length']
const bar: AccessorPair<boolean> = [[true, false], 0]

我的第一次尝试是

type AccessorPair<T> = T extends (infer O)[infer K] ? [O, K] : never

但这只是导致“类型 'K' 不能用于索引类型 'O'。ts(2536)”

我试图通过…来解决

type ValueOf<O, P extends keyof O> = O[P]
type AccessorPair<T> = T extends ValueOf<infer O, infer P> ? [O, P] : never

并且它总是解析为“从不”。

这样的定义对 TypeScript 来说是否过于模糊而无法支持?关于这种类型,我是否还不了解某些编程语言的概念?任何讨论都欢迎!

答案#1

首先,我们需要区分两个定义:TypeScript 中的 数组 和 元组。

将 array 视为一个未知长度的列表,有点像 Rust 中的 vector。

将 tuple 视为一个在编译期间已知长度的列表。

为了处理 [true, false] 元组的允许键,并且它们只有两个 0 | 1,我们需要编写一个小的工具类型:

type IsTuple<T> =
    (T extends Array<any> ?
        (T['length'] extends number
            ? (number extends T['length']
                ? false
                : true)
            : true)
        : false)

{
    type _ = IsTuple<[1, 2]> // true
    type __ = IsTuple<number[]> // false
    type ___ = IsTuple<{ length: 2 }> // false
}

type AllowedTupleLength<
    T extends ReadonlyArray<unknown>,
    Length extends number = never
    > = T extends readonly [infer _, ...infer Tail]
    ? AllowedTupleLength<Tail, Length | Tail['length']>
    : T extends readonly []
    ? Length
    : never;

type Result = AllowedTupleLength<[0,0]> // 1 | 0

现在我们需要一个实用工具,它可以有条件地返回元组和数组的允许键:

type ComputeKeys<Tuple extends any[]> =
    IsTuple<Tuple[0]> extends true ? AllowedTupleLength<Tuple[0]> : keyof Tuple[0]

ComputeKeys 对 [true, false] 返回 1|0,对普通数组返回所有数组键。

顺便问一下,你想允许在元组的第二个元素中使用 forEach, reduce, map 键吗?

此外,我们需要处理所有非数组对象。让我们创建一个包含所有可序列化类型的联合类型 Json :

type Json = | string | number | boolean | { [prop: string]: Json } | Array<Json>

type IsTuple<T> =
    (T extends Array<any> ?
        (T['length'] extends number
            ? (number extends T['length']
                ? false
                : true)
            : true)
        : false)

type AllowedTupleLength<
    T extends ReadonlyArray<unknown>,
    Length extends number = never
    > = T extends readonly [infer _, ...infer Tail]
    ? AllowedTupleLength<Tail, Length | Tail['length']>
    : T extends readonly []
    ? Length
    : never;

type ComputeKeys<Tuple extends any[]> =
    IsTuple<Tuple[0]> extends true ? AllowedTupleLength<Tuple[0]> : keyof Tuple[0]


function handleTuple<
    Elem extends Exclude<Json, any[]>, // overload signature for non array serializable values
    Tuple extends Elem[]
>(tuple: [...Tuple, keyof Tuple[0]]): [...Tuple]
function handleTuple<
    Elem,
    NestedTuple extends Elem[],
    Tuple extends [...NestedTuple][]
>(tuple: [...Tuple, ComputeKeys<Tuple>]): [...Tuple]
function handleTuple(tuple: unknown[]) {
    return tuple
}

handleTuple([[true, false], 1]) // ok
handleTuple([[true, false], 2]) // expected error, index is too big

handleTuple([new Array(), 'length']) // ok
handleTuple([new Array(), 2]) // allowed because we don't know exact length of array
handleTuple([{ name: 'John', age: 32 }, 'name']) // allowed because we don't know exact length of array

操场

顺便说一下,由于你不知道元组中所有允许的对象,没有额外的函数是不可能做到的。