简体   繁体   English

在打字稿中键入索引签名对象类型的安全合并

[英]Type safe merge of index signature object types in typescript

This question and answer covers object literals but the answer does not work when using index signature object types.这个问题和答案涵盖了对象文字,但在使用索引签名对象类型时,答案不起作用。 eg:例如:

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]: ...; }>'`

Code 代码

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 a string literal type , even if you initialize it with a string literal:如果一个值被注释为string类型,那么编译器不会将其缩小为字符串文字 type ,即使您使用字符串文字对其进行初始化:

const str: string = "hello"; // irretrievably widened to string
let onlyHello: "hello" = "hello";
onlyHello = str; //error! string is not assignable to "hello"

In the above, the string variable str is initialized to "hello" , but you cannot assign that to a variable of type "hello" ;在上面, string变量str被初始化为"hello" ,但你不能将它分配给类型为"hello"的变量; the compiler has permanently forgotten that the value of str is the string literal "hello" .编译器永远忘记了str的值是字符串文字"hello"

This "forgetful" widening is true for any annotation of a non-union type.对于非联合类型的任何注释,这种“健忘”的扩展都是正确的。 If the type is a union, the compiler will actually narrow the type of the variable on assignment, at least until the variable is reassigned:如果类型是联合,编译器实际上会在赋值时缩小变量的类型,至少在变量被重新赋值之前:

const strOrNum: string | number = "hello"; // narrowed from string | number to string
let onlyString: string = "hello";
onlyString = strOrNum; // okay, strOrNum is known to be string

Unfortunately, your Obj type is a non-union type.不幸的是,您的Obj类型是非联合类型。 And since it has a string index signature, the compiler will only know that a variable annotated as Obj will have string keys and will not remember the literal value of those keys, even if it is initialized with an object literal with string literal keys:并且由于它具有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

Thus your a and b variables, which have been annotated as type Obj , are known to the compiler only to be of type Obj .因此,您的ab变量已被注释为Obj类型,编译器只知道它们的类型为Obj It has forgotten any individual properties inside them.它已经忘记了它们内部的任何个体属性。 From the type system's point of view, a and b are identical.从类型系统的角度来看, ab是相同的。

And thus no matter what crazy type games I try to play with the signature for mergeUnique() , nothing I can do will make it so that mergeUnique(a, b) succeeds while mergeUnique(a, a) fails;因此,无论我尝试使用mergeUnique()的签名玩什么疯狂类型的游戏,我都无法做到让mergeUnique(a, b)成功而mergeUnique(a, a)失败; the types of a and b are identical non-union types; ab的类型是相同的非联合类型; the compiler can't tell them apart.编译器无法区分它们。


If you want the compiler to remember the individual keys on a and b , you should not annotate them but let the compiler infer them.如果您希望编译器记住ab上的各个键,则不应对其进行注释,而应让编译器推断它们。 If you want to ensure that a and b are assignable to Obj without actually widening them to Obj , you can make a generic helper function to do that:如果您想确保ab可分配给Obj而不实际将它们扩展为Obj ,您可以创建一个通用的辅助函数来做到这一点:

const asObj = <T extends Obj>(t: T) => t;

The function asObj() just returns the same value it receives as an argument, and does not change its inferred type.函数asObj()只返回它作为参数接收的相同值,并且不会更改其推断类型。 But since T is constrained to Obj , it will only succeed if the object could be assigned to Obj :但由于T约束Obj ,只有当对象可以分配给Obj它才会成功:

const a = asObj({ a: undefined }); // {a: undefined}
const b = asObj({ b: 3 }); // {b: number}
const c = asObj({ c: "oopsie" }); // error!

Now you have a and b of narrow types with known string literal property keys, (and a c with a compiler error because "oopsie" is not a `number | undefined).现在你有ab窄类型,具有已知的字符串文字属性键,(以及一个带有编译器错误的c ,因为"oopsie"不是一个 `number | undefined)。 And thus the rest of your code behaves as desired:因此,您的其余代码将按预期运行:

// 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)                    

Okay, hope that helps;好的,希望有帮助; good luck!祝你好运!

Playground link to code Playground 链接到代码

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM