提问人:Hantoa Tenwhij 提问时间:8/30/2023 最后编辑:Hantoa Tenwhij 更新时间:8/30/2023 访问量:37
无法推断扩展基元类型联合的文本类型
Unable to Infer Literal Types that Extend a Union of Primitive Types
问:
我有以下组件和功能:
interface ILabel<T> {
readonly label: string;
readonly key: T
}
interface IProps<T> {
readonly labels: Array<ILabel<T>>;
readonly defaultValue: T;
readonly onChange: (state: ILabel<T>) => void;
}
const testFunc = <T extends string | number>(labels: IProps<T>) => labels
我的目的是能够混合不同类型的键,所以有些可能是字符串,有些可能是数字,有些可能是布尔值。我的目的是从标签中推断出类型,并将其应用于其他道具,例如或受益于文字类型的道具。否则,需要类型保护来确保只有键是其他函数可接受的值,这使工程师对每个用例负责。我想避免这种情况。defaultKey
onSubmit
当所有键都属于同一类型时,我完全能够调用该函数。
testFunc({
labels: [{
label: 'whatever',
key: 'a',
}, {
label: 'whatever',
key: 'b',
}, {
label: 'whatever',
key: 'c',
}],
defaultValue: 'a',
onChange: (state) => {}
}) // Correctly assigns type IProps<'a' | 'b' | 'c'>
但是,每当我尝试混合类型时,打字稿都会认为这是第一个索引的常量,并对所有其他值抛出错误。例:T
testFunc({
labels: [{
label: 'whatever',
key: 'a',
}, {
label: 'whatever',
key: 'b',
}, {
label: 'whatever',
key: 2,
}],
defaultValue: 'c',
onChange: (state) => {}
}) // Errors because it thinks the type is IProps<'a'>
Type '"b"' is not assignable to type '"a"'.ts(2322)
TabBar.types.ts(79, 12): The expected type comes from property 'key' which is declared here on type 'ILabel<"a">'
有一种方法可以通过使用基元来混合它们。例如,以下工作原理:
enum TestEnum {
ONE = 'one',
TWO = 'two',
THREE = 3,
}
enum TestEnum2 {
FOUR = 'four',
FIVE = 'five',
SIX = 6,
}
testFunc({
labels: [{
label: 'whatever',
key: TestEnum.ONE,
}, {
label: 'whatever',
key: TestEnum.TWO,
}, {
label: 'whatever',
key: TestEnum.THREE,
}],
defaultValue: TestEnum.TWO,
onChange: (state) => {}
}) // Correctly works as IProps<TestEnum>
但是,所有键的类型都必须为 。与原始文本或其他枚举值混合也会导致相同的错误。TestEnum
// Mixing different enums
testFunc({
labels: [{
label: 'whatever',
key: TestEnum.ONE,
}, {
label: 'whatever',
key: TestEnum.TWO,
}, {
label: 'whatever',
key: TestEnum2.FOUR,
}],
defaultValue: TestEnum.TWO,
onChange: (state) => {}
}) // Errors because it infers as IProps<TestEnum.ONE>
// Mixing enum and literal
testFunc({
labels: [{
label: 'whatever',
key: TestEnum.ONE,
}, {
label: 'whatever',
key: 'two',
}],
defaultValue: 'two',
onChange: (state) => {}
}) // Errors because it infers as IProps<TestEnum.ONE>
打字稿被混淆有什么解释?还是有合乎逻辑的解释?
我知道混合字符串和数字文字的用例非常好,但工程师可能会尝试混合不同的枚举,或将枚举与文字混合,并对这个模棱两可的错误感到困惑。
答:
TypeScript 倾向于不合成跨越扩大的文字边界的新联合类型(所以 from 和 “ 很好,因为两者都加宽为 ,但 from 和就不好了,因为一个加宽到 ,另一个加宽到 )。这是因为人们经常希望将发生这种情况的代码标记为错误。请参阅为什么类型参数未推断为联合类型?。规范示例是这样的"a" | "b"
"a"
"b
string
"a" | 1
"a"
1
string
number
declare function f<T extends string | number>(x: T, y: T): void;
f("a", "b") // okay
f(1, 2); // okay
f("a", 2); // error
虽然有些人会争辩说它应该成功,但更多的人似乎觉得它应该失败,事实也确实如此。f("a", 2)
microsoft/TypeScript#44312 上有一个开放的功能请求,允许使用一些修饰符声明类型参数,以表达在可能的情况下合成联合的愿望。如果它被实现,也许你可以写一些类似 or 的东西,但现在它不是语言的一部分,你需要解决它。declare function f<allowunion T extends string | number>(x: T, y: T): void
const testFunc = <allowunion T extends string | number>(labels: IProps<T>) => labels
异构数组的常见解决方法是让 type 参数对应于数组类型本身(即单个类型),而不是数组元素类型(可能是联合)。这需要重新表述所有内容:
interface MyProps<T extends Array<string | number>> {
readonly labels: { [I in keyof T]: ILabel<T[I]> };
readonly defaultValue: T[number];
readonly onChange: (state: ILabel<T[number]>) => void;
}
const testFunc = <T extends Array<string | number>>(
labels: MyProps<T>
): IProps<T[number]> => labels;
因此,现在 type 参数对应于元素属性中的字符串/数字数组,当您调用编译器时,会从 的属性的映射数组类型推断。为了引用元素类型,我们只需使用 .T
key
testFunc
T
labels
MyProps<T>
number
让我们来测试一下:
const v = testFunc({
labels: [
{ label: 'whatever', key: 'a', },
{ label: 'whatever', key: 'b', },
{ label: 'whatever', key: 'c', }
],
defaultValue: 'a',
onChange: (state) => { }
}); // okay
// const v: IProps<"a" | "b" | "c">
const w = testFunc({
labels: [
{ label: 'whatever', key: 'a', },
{ label: 'whatever', key: 'b', },
{ label: 'whatever', key: 2, }
],
defaultValue: 2,
onChange: (state) => { }
}); // okay
// const w: IProps<"a" | "b" | 2>
const x = testFunc({
labels: [
{ label: 'whatever', key: TestEnum.ONE, },
{ label: 'whatever', key: TestEnum.TWO, },
{ label: 'whatever', key: TestEnum.THREE, }
],
defaultValue: TestEnum.TWO,
onChange: (state) => { }
}); // okay
// const x: IProps<TestEnum>
const y = testFunc({
labels: [
{ label: 'whatever', key: TestEnum.ONE, },
{ label: 'whatever', key: TestEnum.TWO, },
{ label: 'whatever', key: TestEnum2.FOUR, }
],
defaultValue: TestEnum.TWO,
onChange: (state) => { }
}); // okay
// const y: IProps<TestEnum.ONE | TestEnum.TWO | TestEnum2.FOUR>
const z = testFunc({
labels: [
{ label: 'whatever', key: TestEnum.ONE, },
{ label: 'whatever', key: 'two', }
],
defaultValue: 'two',
onChange: (state) => { }
}); // okay
// const z: IProps<"two" | TestEnum.ONE>
这一切都如愿以偿。
评论
enum
true
const f = <T>(x: T, y: T) => {}
f("a", 1)
...otherProps<T>