繁体   English   中英

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

[英]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

因此,您的ab变量已被注释为Obj类型,编译器只知道它们的类型为Obj 它已经忘记了它们内部的任何个体属性。 从类型系统的角度来看, ab是相同的。

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


如果您希望编译器记住ab上的各个键,则不应对其进行注释,而应让编译器推断它们。 如果您想确保ab可分配给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!

现在你有ab窄类型,具有已知的字符串文字属性键,(以及一个带有编译器错误的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)                    

好的,希望有帮助; 祝你好运!

Playground 链接到代码

暂无
暂无

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

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