typescript中索引签名对象类型的类型安全合并

typescript中索引签名对象类型的类型安全合并,typescript,merge,spread-syntax,Typescript,Merge,Spread Syntax,但当使用索引签名对象类型时,答案不起作用。e、 g: 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>, ) { retur

但当使用索引签名对象类型时,答案不起作用。e、 g:

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 Although there are some techniques to manipulate types with index signatures (see this answer for an example), the specific check you want to happen here is not possible.  If a value is annotated to be of type 
string
, then the compiler will not narrow it down to astring literal type, even if you initialize it with a string literal:

const str: string = "hello"; // irretrievably widened to string
let onlyHello: "hello" = "hello";
onlyHello = str; //error! string is not assignable to "hello"
type UniqueObject={[K in keyof U]:K扩展keyof T?never:U[K]}
导出函数mergeUnique(
a:T,
b:唯一对象,
c?:唯一对象,
) {
返回{
A.
B
C
}
}
类型Obj={[索引:字符串]:数字|未定义}
常量a:Obj={a:undefined}
常数b:Obj={b:3}
//应该都过去了
const res01=mergeUnique({a:undefined},{b:3})
const res02=mergeUnique({a:undefined},b)
const res03=mergeUnique(a,{b:3})//错误不正确❌ `类型“number”不可分配给类型“never”`
const res04=mergeUnique(a,b)//错误不正确❌ `类型“undefined”不可分配给类型“never”`
const res05=mergeUnique({b:3},{a:undefined})
const res06=mergeUnique(b,{a:undefined})//错误不正确❌ `类型“undefined”不可分配给类型“never”`
const res07=mergeUnique({b:3},a)
const res08=mergeUnique(b,a)//错误不正确❌ `“Obj”类型的参数不能分配给“UniqueObject”类型的参数`
//如果一切都失败了
const res09=mergeUnique({a:undefined},{a:undefined})
const res10=mergeUnique({a:undefined},a)//传递错误❌
const res11=mergeUnique(a,{a:undefined})

const res12=mergeUnique(a,a)//错误正确尽管有一些技术可以使用索引签名操作类型(请参见示例),但您希望在此处执行的特定检查是不可能的。如果一个值被注释为
string
类型,则编译器不会将其缩小为a,即使您使用字符串文字对其进行初始化:

const strOrNum: string | number = "hello"; // narrowed from string | number to string
let onlyString: string = "hello";
onlyString = strOrNum; // okay, strOrNum is known to be string
在上面的例子中,
字符串
变量
str
被初始化为
“hello”
,但是您不能将其分配给类型为
“hello”
的变量;编译器永久忘记了
str
的值是字符串literal
“hello”

这种“遗忘”加宽适用于任何非并集类型的注释。如果类型是并集,编译器实际上会在赋值时缩小变量的类型,至少在重新赋值变量之前:

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
不幸的是,您的
Obj
类型是非联合类型。由于它有一个
string
索引签名,编译器只知道注释为
Obj
的变量将有
string
键,并且不会记住这些键的文本值,即使它是用带有字符串文本键的对象文本初始化的:

const asObj = <T extends Obj>(t: T) => t;
因此,编译器只知道注释为类型
Obj
a
b
变量属于类型
Obj
。它忘记了它们里面的任何单独属性。从类型系统的角度来看,
a
b
是相同的

因此,无论我尝试玩什么疯狂类型的游戏,我都无法使
mergeUnique()
成功,而
mergeUnique(a,b)
失败;
a
b
的类型是相同的非联合类型;编译器无法区分它们


如果希望编译器记住
a
b
上的各个键,则不应对它们进行注释,而应让编译器推断它们。如果要确保
a
b
可分配给
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”
不是一个未定义的“数字”)。因此,代码的其余部分的行为符合要求:

好吧,希望这会有帮助;祝你好运


这里唯一让我吃惊的是
mergeUnique({a:undefined},a)
传递。我所期望的其他行为,因为你自愿扩大了
a
b
的类型,使它们比应该的更加模糊。@PatrickRoberts同意。你不认为有更好的类型来提供所需的功能吗?我不知道为什么要在这里问。谢谢。您无法键入
mergeUnique()
使
mergeUnique(a,b)
成功,而
mergeUnique(a,a)
失败;
a
b
的类型是相同的非联合类型;编译器无法区分它们。如果你想让编译器记住
a
b
上的各个键,你不应该对它们进行注释,而是让编译器推断它们(
const a:Obj={…}
是坏的,
const a={…}
是好的)。谢谢@jcalz。这是有道理的。如果你想发布一个答案,我会将其标记为已接受的答案。谢谢@jcalz给出这个令人惊讶的答案。真的很感激!
// 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)