[英]Type safe merge of index signature object types in typescript
这个问题和答案涵盖了对象文字,但在使用索引签名对象类型时,答案不起作用。 例如:
type UniqueObject<T, U> = { [K in keyof U]: K extends keyof T ? never : U[K] }
export function mergeUnique <T, U, V> (
a: T,
b?: UniqueObject<T, U>,
c?: UniqueObject<T & U, V>,
) {
return {
...a,
...b,
...c,
}
}
type Obj = { [index: string]: number | undefined }
const a: Obj = { a: undefined }
const b: Obj = { b: 3 }
// should all pass
const res01 = mergeUnique({ a: undefined }, { b: 3 })
const res02 = mergeUnique({ a: undefined }, b)
const res03 = mergeUnique(a, { b: 3 }) // errors incorrectly ❌ `Type 'number' is not assignable to type 'never'`
const res04 = mergeUnique(a, b) // errors incorrectly ❌ `Type 'undefined' is not assignable to type 'never'`
const res05 = mergeUnique({ b: 3 }, { a: undefined })
const res06 = mergeUnique(b, { a: undefined }) // errors incorrectly ❌ `Type 'undefined' is not assignable to type 'never'`
const res07 = mergeUnique({ b: 3 }, a)
const res08 = mergeUnique(b, a) // errors incorrectly ❌ `Argument of type 'Obj' is not assignable to parameter of type 'UniqueObject<Obj, { [x: string]: ...; }>'`
// should all fail
const res09 = mergeUnique({ a: undefined }, { a: undefined })
const res10 = mergeUnique({ a: undefined }, a) // passes incorrectly ❌
const res11 = mergeUnique(a, { a: undefined })
const res12 = mergeUnique(a, a) // errors correctly 🔸 but reason wrong: `Argument of type 'Obj' is not assignable to parameter of type 'UniqueObject<Obj, { [x: string]: ...; }>'`
尽管有一些技术可以使用索引签名来操作类型(请参阅此答案的示例),但您希望在此处进行的特定检查是不可能的。 如果一个值被注释为string
类型,那么编译器不会将其缩小为字符串文字 type ,即使您使用字符串文字对其进行初始化:
const str: string = "hello"; // irretrievably widened to string
let onlyHello: "hello" = "hello";
onlyHello = str; //error! string is not assignable to "hello"
在上面, string
变量str
被初始化为"hello"
,但你不能将它分配给类型为"hello"
的变量; 编译器永远忘记了str
的值是字符串文字"hello"
。
对于非联合类型的任何注释,这种“健忘”的扩展都是正确的。 如果类型是联合,编译器实际上会在赋值时缩小变量的类型,至少在变量被重新赋值之前:
const strOrNum: string | number = "hello"; // narrowed from string | number to string
let onlyString: string = "hello";
onlyString = strOrNum; // okay, strOrNum is known to be string
不幸的是,您的Obj
类型是非联合类型。 并且由于它具有string
索引签名,编译器将只知道注释为Obj
的变量将具有string
键并且不会记住这些键的文字值,即使它是使用带有字符串文字键的对象文字初始化的:
const obj: Obj = { a: 1, b: 2 }; // irretrievably widened to Obj
let onlyAB: { a: 1, b: 1 } = { a: 1, b: 1 };
onlyAB = obj; // error! Obj is missing a and b
因此,您的a
和b
变量已被注释为Obj
类型,编译器只知道它们的类型为Obj
。 它已经忘记了它们内部的任何个体属性。 从类型系统的角度来看, a
和b
是相同的。
因此,无论我尝试使用mergeUnique()
的签名玩什么疯狂类型的游戏,我都无法做到让mergeUnique(a, b)
成功而mergeUnique(a, a)
失败; a
和b
的类型是相同的非联合类型; 编译器无法区分它们。
如果您希望编译器记住a
和b
上的各个键,则不应对其进行注释,而应让编译器推断它们。 如果您想确保a
和b
可分配给Obj
而不实际将它们扩展为Obj
,您可以创建一个通用的辅助函数来做到这一点:
const asObj = <T extends Obj>(t: T) => t;
函数asObj()
只返回它作为参数接收的相同值,并且不会更改其推断类型。 但由于T
被约束到Obj
,只有当对象可以分配给Obj
它才会成功:
const a = asObj({ a: undefined }); // {a: undefined}
const b = asObj({ b: 3 }); // {b: number}
const c = asObj({ c: "oopsie" }); // error!
现在你有a
和b
窄类型,具有已知的字符串文字属性键,(以及一个带有编译器错误的c
,因为"oopsie"
不是一个 `number | undefined)。 因此,您的其余代码将按预期运行:
// these all succeed
const res01 = mergeUnique({ a: undefined }, { b: 3 })
const res02 = mergeUnique({ a: undefined }, b)
const res03 = mergeUnique(a, { b: 3 })
const res04 = mergeUnique(a, b)
const res05 = mergeUnique({ b: 3 }, { a: undefined })
const res06 = mergeUnique(b, { a: undefined })
const res07 = mergeUnique({ b: 3 }, a)
const res08 = mergeUnique(b, a)
// these all fail
const res09 = mergeUnique({ a: undefined }, { a: undefined })
const res10 = mergeUnique({ a: undefined }, a)
const res11 = mergeUnique(a, { a: undefined })
const res12 = mergeUnique(a, a)
好的,希望有帮助; 祝你好运!
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.