无法推断扩展原始类型联合的文字类型

问题描述 投票:0回答:1

我有以下组件和功能:

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 混淆有什么解释?或者有没有合理的解释?

我理解混合字符串和数字文字的用例非常好,但工程师很可能会尝试混合不同的枚举,或枚举与文字,并对这种不明确的错误感到困惑。

游乐场链接

typescript casting
1个回答
0
投票

TypeScript 往往不会合成新的 union 类型,跨越扩大的文字边界(因此

"a" | "b"
"a"
中的
"b
很好,因为两者都扩大到
string
,但是
"a" | 1
中的
"a"
1
不好,因为一个扩展为
string
,另一个扩展为
number
)。这是因为人们经常希望将发生这种情况的代码标记为错误。请参阅 Why isn't the type argument推断为联合类型?。典型的例子是这样的

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 有一个开放功能请求,允许使用某种修饰符来声明类型参数,以表达在可能的情况下合成联合的愿望。如果实现了,也许您可以编写类似

declare function f<allowunion T extends string | number>(x: T, y: T): void
const testFunc = <allowunion T extends string | number>(labels: IProps<T>) => labels
的内容,但目前它不是语言的一部分,您需要解决它。


异构数组的常见解决方法是让类型参数对应于数组类型本身(这是单一类型),而不是数组 element 类型(这可能是联合)。这需要重新表述一切:

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;

所以现在类型参数

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>
这一切都按预期工作。

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.