我有以下组件和功能:
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 往往不会合成新的 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>
这一切都按预期工作。