TypeScript是一个结构类型系统。这意味着只要你的数据结构满足契约,TypeScript就会允许它。即使你有太多的键声明。
type Person = {
first: string, last: string
}
declare function savePerson(person: Person);
const tooFew = { first: 'Stefan' };
const exact = { first: 'Stefan', last: 'Baumgartner' }
const tooMany = { first: 'Stefan', last: 'Baumgartner', age: 37 }
savePerson(tooFew); // 💥 doesn't work
savePerson(exact); // ✅ satisfies the contract
savePerson(tooMany); // ✅ satisfies the contract
这很好地补充了JavaScript的工作方式,给你带来了灵活性和类型安全。在某些情况下,你可能想要一个对象的确切形状。例如,当你向后端发送数据时,如果它得到了太多的信息就会出错。
savePerson(tooMany); // ✅ satisfies the contract, 💥 bombs the backend
在JS世界中,总是确保在这样的场景中明确地发送有效载荷,不要仅仅依赖类型。但是,虽然类型不能帮助你获得100%正确的通信,但我们可以在编译时得到一点帮助,确保我们不会偏离自己的轨道。所有这些都有条件类型的帮助。
首先,我们检查我们要验证的对象是否与原始形状相匹配。
type ValidateShape<T, Shape> =
T extends Shape ? ...
通过这个调用,我们确保我们作为参数传递的对象是Shape 的一个子类型。然后,我们检查任何额外的键。
type ValidateShape<T, Shape> =
T extends Shape ?
+ Exclude<keyof T, keyof Shape> extends never ? ...
那么,这是如何工作的?Exclude<T, U> 被定义为T extends U ? never : T 。我们传入的键是要验证的对象和形状。比方说,Person 是我们的形状,而tooMany = { first: 'Stefan', last: 'Baumgartner', age: 37 } 是我们要验证的对象。这就是我们的键。
keyof Person = 'first' | 'last'
keyof typeof tooMany = 'first' | 'last' | 'age'
'first' 和 都在联盟类型中,所以它们返回 , 返回自己,因为它在 中不可用。'last' never age Person
keyof Person = 'first' | 'last'
keyof typeof tooMany = 'first' | 'last' | 'age'
Exclude<keyof typeof tooMany, keyof Person> = 'age';
是否完全匹配,Exclude<T, U> 返回never 。
keyof Person = 'first' | 'last'
keyof typeof exact = 'first' | 'last'
Exclude<keyof typeof exact, keyof Person> = never;
在ValidateShape 中,我们检查Exclude 是否扩展了never ,这意味着我们没有任何extrac键。如果这个条件为真,我们返回我们要验证的类型。在所有其他条件下,我们返回never 。
type ValidateShape<T, Shape> =
T extends Shape ?
Exclude<keyof T, keyof Shape> extends never ?
+ T : never : never;
让我们调整一下我们的原始函数。
declare function savePerson<T>(person: ValidateShape<T, Person>): void;
有了它,就不可能传递那些与我们期望的类型形状不完全一致的对象。
savePerson(tooFew); // 💥 doesn't work
savePerson(exact); // ✅ satisfies the contract
savePerson(tooMany); // 💥 doesn't work