这个答案解决了如何计算文字初始值设定项(例如 { value: 7, data: 'test', note: 'hello' } 到对象类型的联合,例如 type sth={ value: number, data: string } | { value: number, note: string } 的分配的验证,而不忽略任何未指定的多余属性。
这里介绍的类型函数相当于above solution of @jcalz中的ExclusifyUnion。然而,它不仅仅是使用相同输入但编码略有不同的另一种类型函数。相反,这里介绍的功能使用附加输入,如下所述。
将文字初始值设定项的类型作为额外参数添加到类型函数
考虑以下语句:
type T1 = {<some props>}
type T2 = {<some props>}
type T3 = {<some props>}
type TU=T1|T2|T3
SomeTypeDef<T> = ...
const t:SomeTypeDef<TU> = {a:1,b:2}
最后一行是赋值语句。分配中发生的处理有两个不同且独立的部分:
- 隔离的左侧是类型函数
SomeTypeDef,带有单个输入变量TU。
- 确定 r.h.s. 分配的有效性。文字初始值设定项
{<some props>} 为 l.h.s 类型。该计算使用无法更改的 Typescript 固定分配规则进行。
现在假设我们定义了一个附加类型
type I = {a:1,b:2}
您会注意到 r.h.s. 上的字面量初始值设定项的类型。的任务。现在假设我们将该类型作为附加变量添加到 l.h.s. 上的类型函数:
const t:SomeTypeDefPlus<TU,I> = {a:1,b:2}
现在 l.h.s 类型函数有额外的要处理的信息。所以SomeTypeDef<TU>可以表达什么,SomeTypeDefPlus<TU,I>也可以用同样的长度编码表达。然而,SomeTypeDefPlus<TU,I> 可能比 SomeTypeDef<TU> 表达更多的东西,和/或可能能够用更短的代码表达相同的东西。在伪伪代码中:
Expressability(SomeTypeDefPlus<TU,I>) >= Expressability(SomeTypeDef<TU>)
你应该反对,因为
- 写入类型
type I = {<some props>},并且
- 并编写 r.h.s 文字初始值设定项
.... = {<some props>}
是两倍的写作——代码长度的惩罚。确实如此。这个想法是——如果值得的话——最终将启用一种方法来从 r.h.s 初始值设定项推断类型 I,例如,预处理或新的打字稿语言功能。毕竟,静态信息 {<some props>} 就在那里,但由于设计技巧而无法访问,这有点愚蠢。
下面给出了代码演示,然后进行了讨论。
// c.f. https://github.com/microsoft/TypeScript/issues/42997
// craigphicks Feb 2021
//-----------------------
// TYPES
type T1 = {a:number,b:number}
type T2 = {a:number,c:number}
type T3 = {a:string,c?:number}
type T4 = {a:bigint, [key:string]:bigint}
type T5 = {a:string, d:T1|T2|T3|T4}
type T12 = T1|T2|T3|T4|T5
//-----------------------
// TYPES INFERRED FROM THE INITIALIZER
type I0 = {}
type I1 = {a:1,b:1}
type I2 = {a:1,c:1}
type I3 = {a:1,b:1,c:1}
type I4 = {a:1}
type I5 = {a:'2',c:1}
type I6 = {a:'2'}
type I7 = {a:1n, 42:1n}
type I8 = {a:'1', d:{a:1n, 42:1n}}
type I9 = {a:'1', d:{}}
//-----------------------
// THE CODE
type Select<T,I>= {[P in keyof I]: P extends keyof T ?
(T[P] extends object ? ExclusifyUnionPlus<T[P],I[P]> : T[P]) : never}
type ExclusifyUnionPlus<T,I>= T extends any ? (I extends Select<T,I> ? T : never):never
//-----------------------
// case specific type aliases
type DI<I>=ExclusifyUnionPlus<T12,I>
// special types for se question https://stackoverflow.com/q/46370222/4376643
type sth = { value: number, data: string } | { value: number, note: string };
type DIsth<I>=ExclusifyUnionPlus<sth,I>
//-----------------------
// THE TESTS - ref=refuse, acc=accept
const sth0:DIsth<{ value: 7, data: 'test' }>={ value: 7, data: 'test' }; // should acc
const sth1:DIsth<{ value: 7, note: 'test' }>={ value: 7, note: 'test' }; // should acc
const sth2:DIsth<{ value: 7, data:'test', note: 'hello' }>={ value:7, data:'test',note:'hello' }; // should ref
type DI0=DI<I0> ; const d0:DI0={} // should ref
type DI1=DI<I1> ; const d1:DI1={a:1,b:1} // T1, should acc
type DI2=DI<I2> ; const d2:DI2={a:1,c:1} // T2, should acc
type DI3=DI<I3> ; const d3:DI3={a:1,b:1,c:1} // should ref
type DI4=DI<I4> ; const d4:DI4={a:1} // should ref
type DI5=DI<I5> ; const d5:DI5={a:'2',c:1} // T3, should acc
type DI6=DI<I6> ; const d6:DI6={a:'2'} // T3, should acc
type DI7=DI<I7> ; const d7:DI7={a:1n,42:1n} // T4, should acc
type DI8=DI<I8> ; const d8:DI8={a:'1',d:{a:1n,42:1n}} // T5, should acc
type DI9=DI<I9> ; const d9:DI9={a:'1',d:{}} // should ref
//-------------------
// Comparison with type function NOT using type of intializer
// Code from SE https://stackoverflow.com/a/46370791/4376643
type AllKeys<T> = T extends unknown ? keyof T : never;
type Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
type _ExclusifyUnion<T, K extends PropertyKey> =
T extends unknown ? Id<T & Partial<Record<Exclude<K, keyof T>, never>>> : never;
type ExclusifyUnion<T> = _ExclusifyUnion<T, AllKeys<T>>;
//-------------------
// case specific alias
type SU=ExclusifyUnion<T12>
// tests
const sd0:SU={} // should ref
const sd1:SU={a:1,b:1} // should acc
const sd2:SU={a:1,c:1} // should acc
const sd3:SU={a:1,b:1,c:1} // should ref
const sd4:SU={a:1} // should ref
const sd5:SU={a:'2',c:1} // should acc
const sd6:SU={a:'2'} // should acc
const sd7:SU={a:1n,42:1n} // should acc
const sd8:SU={a:'1',d:{a:1n,42:1n}} // should acc
const sd9:SU={a:'1',d:{}} // should ref
// Apparently ExclusifyUnion doesn't handle addtional property speficier in T4
// Also does it handle deep objects? Have posted message to ExclusifyUnion author, awaiting reply.
Typescript Playground
讨论
深层对象的代码递归 - ExclusifyUnionPlus<T,I> 调用 Select 和 Select 然后当属性本身是对象时递归调用 ExclusifyUnionPlus<T[P],I[P]>。
不包括一些边缘情况,例如成员函数。
测试
测试用例包括
结论
除了需要两次进入实例之外,所提出的范例(将初始化类型添加到 lhs 函数)被证明可以在几个检测多余属性的测试用例中正常运行。
我们可以判断在 l.h.s 中添加初始化器类型的实用价值。通过根据以下两个标准比较 ExclusifyUnion 和 ExclusifyUnionPlus 来键入函数:
至于“简单明了”,ExclusifyUnionPlus 似乎更易于编码和理解。另一方面,两次编写初始化程序是不方便的。我已经提交了a proposal to Typescript issues建议类似的东西
const t:SomeTypeDefPlus<TU,I> = {a:1,b:2} as infer literal I
会有帮助的。
至于“总表达范围”,目前还不得而知。