This question and answer covers object literals,但使用索引签名对象类型时答案不起作用。例如:
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]: ...; }>'`
答案 0 :(得分:1)
尽管有一些使用索引签名操作类型的技术(例如,请参见this answer),但是您想要在此处进行的特定检查是不可能的。如果将一个值注释为string
类型,那么即使您使用字符串文字对其进行初始化,编译器也不会将其范围缩小到string literal 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
,则可以使用generic辅助函数来做到这一点:
const asObj = <T extends Obj>(t: T) => t;
函数asObj()
仅返回与参数相同的值,并且不更改其推断的类型。但是由于T
是constrained到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"
不是一个数字|未定义)。因此,其余代码的行为符合预期:
// 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)
好的,希望能有所帮助;祝你好运!