首页 > 解决方案 > 为什么我不能在 const 数组中创建一种有效索引?

问题描述

考虑以下:


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 编译器完全知道 STATES,所以它的长度也是 (4)。那么为什么StateIndex评估的类型为

number | keyof STATES

并不是

0 | 1 | 2 | 3

?

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

type StateIndex = 0 | 1 | 2 | 3
type State = typeof States[StateIndex]

但是编译器是否没有对有效索引的文字类型联合进行评估所需的所有信息?还有另一种方法可以将类型系统推向正确的答案吗?

标签: typescripttuplestype-inference

解决方案


typeof STATE实际上是一个 ReadonlyArray。

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>

我们几乎做到了。但是由于某种原因,TypeScript 返回字符串化的键而不是数值。

可以以通用方式将它们转换为数字。

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>

您可以在此问题/答案和/或我的文章中找到更多上下文和解释

操场

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

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联合

操场

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

只是想展示不同的方式。


推荐阅读