TypeScript a | b允许

时间:2017-09-22 17:29:44

标签: typescript

我很惊讶地发现TypeScript不会抱怨我做这样的事情:

type sth = { value: number, data: string } | { value: number, note: string };
const a: sth = { value: 7, data: 'test' };
const b: sth = { value: 7, note: 'hello' };
const c: sth = { value: 7, data: 'test', note: 'hello' };

我认为可能value被选为类型联合判别式或其他东西,因为我唯一可以解释的是,如果TypeScript以某种方式将number理解为超集例如1 | 2

所以我在第二个对象上将value更改为value2

type sth = { value: number, data: string } | { value2: number, note: string };
const a: sth = { value: 7, data: 'test' };
const b: sth = { value2: 7, note: 'hello' };
const c: sth = { value: 7, data: 'test', note: 'hello' };

仍然没有抱怨,我能够构建c。虽然智能感知在c上出现故障,但当我.进入时它不会提出任何建议。如果我将value中的c更改为value2,则相同。

为什么这不会产生错误?很明显,我没有提供一种类型或另一种类型,而是提供了两者的奇怪组合!

3 个答案:

答案 0 :(得分:4)

另一种选择是使用可选的public static List<ArrayList<String>> allPaths = new ArrayList<>(); for(ArrayList<String> path : allPaths){ List<String> lastEdgePath = Collections.singletonList(path.get(path.size() - 3) + path.get(path.size() - 2) + path.get(path.size() - 1)); } 属性来明确禁止联合中两种类型的字段的混合:

never

type sth = { value: number, data: string; note?: never; } | { value: number, note: string; data?: never; }; const a: sth = { value: 7, data: 'test' }; const b: sth = { value: 7, note: 'hello' }; const c: sth = { value: 7, data: 'test', note: 'hello' }; // ~ Type '{ value: number; data: string; note: string; }' // is not assignable to type 'sth'. 库中有一个XOR generic,可用于帮助您构建如下所示的互斥并集:

ts-essentials

最后一个例子是playground link

答案 1 :(得分:2)

问题Microsoft/TypeScript#14094中的讨论与此相关。

TypeScript中的类型是 open ,因为对象必须至少类型描述的属性才能匹配。因此,对象{ value: 7, data: 'test', note: 'hello' }与类型{ value: number, data: string }匹配,即使它具有多余的note属性。因此,您的c变量确实是有效的sth。如果缺少联盟的某些组成部分所需的所有属性,它将不会是sth

// error: missing both "data" and "note"
const oops: sth = { value: 7 };  

但是:当您在TypeScript中为类型变量分配新的对象文字时,它会执行excess property checking以尝试防止错误。这具有在该赋值期间“关闭”TypeScript的开放类型的效果。这适用于您对接口类型的期望。但对于工会而言,TypeScript目前(如this comment中所述)仅抱怨未在任何成员身上出现的属性。所以以下仍然是一个错误:

// error, "random" is not expected:
const alsoOops: sth = { value: 7, data: 'test', note: 'hello', random: 123 };

但TypeScript目前(截至v2.5)不会以您想要的严格方式对联合类型进行多余的属性检查,它会针对每种组成类型检查对象文字,并在所有组件类型中都有额外的属性进行投诉他们在TypeScript 2.6中,看起来会有改进的excess property checking on discriminated unions,这将缓解这种情况。我不确定这是否适用于你的案例,但是sth的定义都没有被区分(意思是:有一个属性,其文字类型恰好选择了一个联盟的成分)。

所以,除非更改,否则最好的解决方法可能是在使用对象文字时通过明确指定目标成分然后如果需要扩展到联合来避免使用联盟:

type sthA = { value: number, data: string };
type sthB = { value: number, note: string };
type sth = sthA | sthB;

const a: sthA = { value: 7, data: 'test' };
const widenedA: sth = a;
const b: sthB = { value: 7, note: 'hello' };
const widenedB: sth = b;
const c: sthA = { value: 7, data: 'test', note: 'hello' }; // error as expected
const widenedC: sth = c; 
const cPrime: sthB = { value: 7, data: 'test', note: 'hello' }; // error as expected
const widenedCPrime: sth = cPrime; 

希望有所帮助。祝你好运!

答案 2 :(得分:1)

这个答案解决了如何在不忽略任何未指定的多余属性的情况下计算字面初始值设定项(例如,{ 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>},AND
  • 并编写 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> 调用 SelectSelect,然后在属性本身是对象时递归调用 ExclusifyUnionPlus<T[P],I[P]>

不包括某些边缘情况,例如成员函数。

测试

测试用例包括

  • 其他键
  • 深层对象(虽然只有 2 级)

结论

除了需要两次输入实例外,所提议的范式(向 lhs 函数添加初始化器类型)在几个检测多余属性的测试用例中显示可以正常运行..

我们可以判断在l.h.s.中添加初始化类型的实用价值。根据这两个条件通过比较 ExclusifyUnionExclusifyUnionPlus 来键入函数:

  • 简单明了:
  • 表达的总范围:

至于“易用性和清晰度”,ExclusifyUnionPlus 似乎更易于编码和理解。另一方面,两次编写初始化程序是不方便的。我已提交 a proposal to Typescript issues 建议类似

const t:SomeTypeDefPlus<TU,I> = {a:1,b:2} as infer literal I

会有帮助。

至于“表达的总范围”,目前还不得而知。