问题描述
考虑以下事项:
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 | 3
,STATES
还有其他键。
因此,让我们尝试提取所有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
虽然第一个示例可能看起来像是对问题的过度设计,但它可能对其他用例有帮助。
只是想展示不同的方式。