为什么我不能在常量数组中创建一种有效的索引?

人气:80 发布:2023-01-03 标签: type-inference tuples typescript

问题描述

考虑以下事项:


const STATES = ["Todo", "In Progress", "Blocked", "Done"] as const;


type State = typeof STATES[number]; // "Todo" | "In Progress" | "Blocked" | "Done"
type StateIndex = keyof typeof STATES; // number | keyof STATES

// so this works:
let goodIndex: StateIndex = 0;

// but so does this
let badIndex : StateIndex = 42;
在本例中,TS编译器完全知道State,因此其长度为Too(4)。 那么为什么StateIndex的类型求值为

number | keyof STATES

而不是

0 | 1 | 2 | 3

我知道可以用其他方式定义类型:

type StateIndex = 0 | 1 | 2 | 3
type State = typeof States[StateIndex]
但是,编译器不是已经具备了计算有效索引的文字类型联合所需的所有信息吗?是否有其他方法可以将类型系统推向正确答案?

推荐答案

typeof STATE实际上是只读数组。

ReadonlyArray又有这个接口:

interface ReadonlyArray<T> {
    readonly length: number;
    readonly [n: number]: T;
    // other methods
}

因此,我们知道一般类型number被允许作为不可变数组/元组的索引。

我们来看看STATES的所有键:

const STATES = ["Todo", "In Progress", "Blocked", "Done"] as const;

type STATES = typeof STATES

type State = STATES[number]; // "Todo" | "In Progress" | "Blocked" | "Done"

type StateIndex = keyof (typeof STATES); // number | keyof STATES

type StateKeys = {
    [Prop in StateIndex]: Prop
}

您可能已经注意到,除了预期的键0 | 1 | 2 | 3STATES还有其他键。

因此,让我们尝试提取所有number键:

const STATES = ["Todo", "In Progress", "Blocked", "Done"] as const;

type STATES = typeof STATES

type State = STATES[number];

type StateIndex = keyof (typeof STATES);

type FilterNumbers<T extends PropertyKey> = T extends `${number}` ? T : never

// "0" | "1" | "2" | "3"
type Indexes = FilterNumbers<StateIndex>

我们差一点就做到了。但由于某些原因,TypeScrip返回的是字符串键而不是数值。

可以通过泛型方式将它们转换为数字。

type MAXIMUM_ALLOWED_BOUNDARY = 999

type ComputeRange<
    N extends number,
    Result extends Array<unknown> = [],
    > =
    (Result['length'] extends N
        ? Result
        : ComputeRange<N, [...Result, Result['length']]>
    )

type NumberRange = ComputeRange<MAXIMUM_ALLOWED_BOUNDARY>[number]

type FilterNumbers<T extends PropertyKey> = T extends `${number}` ? T : never

type ToNumber<T extends string, Range extends number> =
    (T extends string
        ? (Range extends number
            ? (T extends `${Range}`
                ? Range
                : never)
            : never)
        : never)

const STATES = ["Todo", "In Progress", "Blocked", "Done"] as const;

type STATES = typeof STATES

type State = STATES[number];

type GetNumericKeys<T extends PropertyKey> = ToNumber<FilterNumbers<T>, NumberRange>

// 0 | 1 | 2 | 3
type Result = GetNumericKeys<keyof STATES>

您可以在this question/answer和/或我的article

中找到更多上下文和说明

Playground

还有另一种方法可以从元组中获取允许的索引。

const STATES = ["Todo", "In Progress", "Blocked", "Done"] as const;

type STATES = typeof STATES

type State = STATES[number];


type AllowedIndexes<
    Tuple extends ReadonlyArray<any>,
    Keys extends number = never
    > =
    (Tuple extends readonly []
        ? Keys : (Tuple extends readonly [infer _, ...infer Tail]
            ? AllowedIndexes<Tail, Keys | Tail['length']>
            : Keys
        )
    )

// 0 | 3 | 2 | 1
type Keys = AllowedIndexes<STATES>

您只需递归迭代一个元组并将当前元组的长度推入Keys联合

Playground

虽然第一个示例可能看起来像是对问题的过度设计,但它可能对其他用例有帮助。

只是想展示不同的方式。

16