TypeScript:如何匹配准确的对象形状

220 阅读2分钟

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