TypeScrip不想帮助强制执行约束,因为它违反了类型系统的规则,例如:union的每个成员都可以分配给并集(T
可以分配给T | U
,T
和U
都可以分配给T | U
),intersection可以分配给交集的每个成员(T & U
可以分配给T
和U
).这意味着接受number | {a: number}
的正常函数也应该接受number
,因此也应该接受number & {a: string}
.同样,它应该接受{a: number}
,因此也就是string & {a: number}
.如果您发现自己想要阻止这样的任务,您可能希望花一些时间判断您的用例,以确保您有一个非常好的理由这样做,因为您将使用打字打字系统.我会假设从现在开始我们确实想要继续进行.
在TypeScrip中,primitive types与对象类型的交集是一种方便的虚构,之所以允许这样做,是因为它们有助于模拟nominal types.请参见the FAQ entry for "Can I make type aliases nominal?".这些被称为"品牌原语".从技术上讲,这种类型是不可能存在的,因此"应该"减少到the never
type种.事实是,这种情况没有发生,这意味着我们可以预料到会看到一些奇怪的异常行为.
特别奇怪的是,string & {a: number}
被视为既可以赋值给string
,也可以赋值给object
,尽管string & object
是never
.如果您试图表达"Anumber
不是object
"或"{a: number}
不是object
",那么对于任何合理的实现,它们最终看起来都只是number
和{a: number}
,并且没有任何变化.
我唯一能想到的表达方式就是通过generics和conditional types.我们没有判断,比如说string & object
(never
),而是判断了类似T extends string ? T extends object ? ⋯ : ⋯ : ⋯
的东西.
以下是一种可能的实现:
type Primitive = string | number | bigint | null | undefined | boolean | symbol;
function fn<T extends number | { a: number }>(
_: T extends object ? T extends Primitive ? number & { a: number } : T : T
) { }
我已经将Primitive
定义为所有基元类型的并集,这样我们就可以防止所有(非number
)基元类型,而不仅仅是string
.支票T extends object ? T extends Primitive ? ⋯ : T : T
基本上将仅捕获可分配给Primitive
和object
两者的标记基元.然后,一旦我们知道我们有一个标记的原语,我们就可以确保我们只接受您想要接受的原语,即number & {a: number}
.
让我们测试一下:
fn(1); // okay
fn("abc"); // error
fn({ a: 9 }); // okay
fn({ a: "abc" }); // error
fn(Object.assign(1, { a: 9 })); // okay
fn(Object.assign("abc", { a: 9 })); // error
fn(Object.assign(1, { a: "abc" }); // error
看上go 不错.
Playground link to code个