您可能知道,TypeScript 中的对象类型通过以下方式进行匹配结构子类型因此open and 可扩展的. If Base
是一个对象类型并且Sub extends Base
,那么一个Sub
是一种Base
,并且您应该被允许使用Sub
无论何处Base
是必须的:
const sub: Sub = thing;
const base: Base = sub; // okay because every Sub is also a Base
这意味着每个已知的属性Base
必须是已知的属性Sub
。但反过来并不意味着:并不是每个已知的属性Sub
必须是已知的属性Base
。事实上,您可以轻松地将新的已知属性添加到Sub
在不违反结构子类型的情况下:
interface Base {
baseProp: string;
}
interface Sub extends Base {
subProp: number;
}
const thing = { baseProp: "", subProp: 123 };
A Sub
仍然是一种Base
即使它有额外的属性。所以Base
的属性不能仅限于那些known由编译器。
因此,TypeScript 中的对象类型是开放且可扩展的,而不是closed或“精确”。目前 TypeScript 中没有以下形式的特定类型Exact<Sub>
它只允许已知的属性Sub
存在并拒绝任何具有额外属性的东西。有一个长期开放的请求:微软/TypeScript#12936支持这种“精确类型”,但这不是该语言目前所具备的。
让事情变得复杂的是,对象字面量确实经历所谓的“超额财产检查”,其中意外的属性会产生编译器警告。但这更像是一个 linter 规则,而不是类型系统功能。即使以下内容会产生编译器警告:
const excessProp: Base =
{ baseProp: "", subProp: 123 }; // error!
// ---------------> ~~~~~~~~~~~~
// Object literal may only specify known properties,
// and 'subProp' does not exist in type 'Base'
这并不意味着分配给的值excessProp
是无效的Base
。您仍然可以这样做:
const excessProp = { baseProp: "", subProp: 123 };
const stillOkay: Base = excessProp; // no error
因此,从某种意义上说,确实没有办法在所有可能的情况下强制执行您正在寻找的约束。无论我们提出什么解决方案,总有人可以做到这一点:
const x = { a: 1, c: 1 } as const; // okay
const y: { a: 1 } = x; // okay
magicalFunction(y); // okay
这可能不太可能,您不想担心实施magicalFunction()
防御它,但要记住这一点。额外的属性不会违反 TypeScript 对象类型,因此额外的属性可能会潜入。
无论如何,虽然目前还没有办法说像这样的特定类型ProjectionMap<SomeType>
不能包含多余的属性,可以使其成为自引用通用约束形式的P extends ProjectionMap<SomeType, P>
。你可以想到P
作为要检查的候选类型。如果P
某处有多余的属性,你可以使ProjectionMap<SomeType, P>
与它不兼容。
这基本上是模拟精确类型的解决方法。而不是具体的Exact<T>
,我们有通用的X extends Exactly<T, X>
当且仅当X
可分配给T
但没有多余的属性。看这条评论有关详细信息,请访问 microsoft/TypeScript#12936。
这里是:
type Exactly<T, X> = T & Record<Exclude<keyof X, keyof T>, never>
所以类型Exactly<T, X>
uses the Exclude<T, U>实用型采取键列表 from X
哪些不存在于T
...也就是多余的钥匙。它用the Record<K, V>实用型创建一个对象类型,其键是这些多余的键,其值是the never type,JavaScript 中无法为其分配任何值。例如,Exactly<{a: string}, {a: string, b: number}>
将相当于{a: string, b: never}
。有了那个never
那里有什么使X extends Exactly<T, X>
失败时X
有钥匙不在T
.
现在我们可以使用Exactly
递归地内部ProjectionMap
定义(即使在嵌套对象类型中也禁止多余的键):
type ProjectionMap<T, P extends ProjectionMap<T, P> = T> = Exactly<{
[K in keyof T]?: T[K] extends object
? 1 | ProjectionMap<T[K], P[K]>
: 1 }, P>
进而Projection
没有改变:
type Projection<T, P extends ProjectionMap<T>> = {
[K in keyof T & keyof P]: P[K] extends object
? Projection<T[K], P[K]> : T[K]
} extends infer O ? { [K in keyof O]: O[K] } : never
And magicalFunction
限制P
to ProjectionMap<SomeType, P>
:
type SomeType = {
a: string
b: { a: string, b: string }
}
declare function magicalFunction<
P extends ProjectionMap<SomeType, P>
>(p: P): Projection<SomeType, P>
我们来测试一下:
magicalFunction({ a: 1 }) // {a: string}
magicalFunction({ b: 1 }) // { b: { a: string, b: string } }
magicalFunction({ b: { a: 1 } }) // { b: { a: string } }
magicalFunction({ a: 1, c: 1 }) // error, no 'c' on SomeType
magicalFunction({ b: { a: 1, z: 1 } }) // error, no 'z' on SomeType["b"]
看起来不错!您想受理的案件全部受理,您想驳回的案件全部驳回。当然magicalFunction(y)
之前的问题并没有消失,但是这个实现可能足以满足您需要支持的用例。
Playground 代码链接