打字稿中索引签名对象类型的类型安全合并
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]: ...; }>'`
尽管有一些技术可以使用索引签名来操作类型(参见 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,即使您使用字符串文字对其进行初始化:
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()
只是 returns 它作为参数接收的相同值,并且不会更改其推断类型。但是由于 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)
好的,希望对你有帮助;祝你好运!
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
, then the compiler will not narrow it down to astring 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()
只是 returns 它作为参数接收的相同值,并且不会更改其推断类型。但是由于 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)
好的,希望对你有帮助;祝你好运!