在JavaScript中,你可以用Object.defineProperty ,即时定义对象属性。如果你希望你的属性是只读的或类似的,这很有用。想想看,一个存储对象有一个不应该被改写的最大值。
const storage = {
currentValue: 0
}
Object.defineProperty(storage, 'maxValue', {
value: 9001,
writable: false
})
console.log(storage.maxValue) // 9001
storage.maxValue = 2
console.log(storage.maxValue) // still 9001
defineProperty 和属性描述符是非常复杂的。它们允许你用属性做一切通常为内置对象保留的事情。所以它们在大型代码库中很常见。TypeScript--在写这篇文章的时候--有一个小问题, 。defineProperty
const storage = {
currentValue: 0
}
Object.defineProperty(storage, 'maxValue', {
value: 9001,
writable: false
})
// 💥 Property 'maxValue' does not exist on type...
console.log(storage.maxValue)
如果我们不明确地进行类型转换,我们就不会得到maxValue 附带的类型storage 。然而,对于简单的用例,我们可以提供帮助
断言签名#
在TypeScript 3.7中,该团队引入了断言签名。想想看,一个assertIsNumber 函数,你可以确保一些值的类型是number 。否则,它会抛出一个错误。这类似于Node.js中的assert 函数。
function assertIsNumber(val: any) {
if (typeof val !== "number") {
throw new AssertionError("Not a number!");
}
}
function multiply(x, y) {
assertIsNumber(x);
assertIsNumber(y);
// at this point I'm sure x and y are numbers
// if one assert condition is not true, this position
// is never reached
return x * y;
}
为了遵守这样的行为,我们可以添加一个断言签名,告诉TypeScript我们对这个函数之后的类型有更多了解。
- function assertIsNumber(val: any) {
+ function assertIsNumber(val: any) : asserts val is number
if (typeof val !== "number") {
throw new AssertionError("Not a number!");
}
}
这很像类型谓词,但没有基于条件的结构的控制流,如if 或switch 。
function multiply(x, y) {
assertIsNumber(x);
assertIsNumber(y);
// Now also TypeScript knows that both x and y are numbers
return x * y;
}
如果你仔细观察,你可以看到那些断言签名可以在飞行中改变参数或变量的类型。这也正是Object.defineProperty 所做的。
自定义Property#
免责声明:下面的帮助程序并不以100%准确或完整为目标。它可能会有错误,它可能不会处理
defineProperty规范的每一种边缘情况。然而,它可能会很好地处理很多用例。所以使用它的风险由你自己承担。
就像hasOwnProperty一样,我们创建一个模仿原始函数签名的辅助函数。
function defineProperty<
Obj extends object,
Key extends PropertyKey,
PDesc extends PropertyDescriptor>
(obj: Obj, prop: Key, val: PDesc) {
Object.defineProperty(obj, prop, val);
}
我们用3个泛型来工作。
- 我们要修改的对象,类型为
Obj,它是一个子类型。object - 类型
Key,是PropertyKey(内置)的一个子类型,所以string | number | symbol。 PDesc,是PropertyDescriptor(内置)的一个子类型。这允许我们定义具有所有特性的属性(可写性、可枚举性、可重构性)。
我们使用泛型,因为TypeScript可以将它们缩小到一个非常具体的单元类型。例如,PropertyKey 是所有的数字、字符串和符号。但如果我使用Key extends PropertyKey ,我可以精确到prop ,例如,"maxValue" 的类型。如果我们想通过添加更多的属性来改变原来的类型,这就很有帮助。
Object.defineProperty 函数要么改变对象,要么在出错时抛出一个错误。这正是一个断言函数的作用。我们的自定义助手defineProperty ,因此也是这样做的。
让我们添加一个断言签名。一旦defineProperty 成功执行,我们的对象就有了另一个属性。我们要为此创建一些帮助器类型。签名第一。
function defineProperty<
Obj extends object,
Key extends PropertyKey,
PDesc extends PropertyDescriptor>
- (obj: Obj, prop: Key, val: PDesc) {
+ (obj: Obj, prop: Key, val: PDesc):
+ asserts obj is Obj & DefineProperty<Key, PDesc> {
Object.defineProperty(obj, prop, val);
}
obj 则是 (通过泛型缩小)的类型,以及我们新定义的属性。Obj
这就是DefineProperty 帮助类型。
type DefineProperty<
Prop extends PropertyKey,
Desc extends PropertyDescriptor> =
Desc extends { writable: any, set(val: any): any } ? never :
Desc extends { writable: any, get(): any } ? never :
Desc extends { writable: false } ? Readonly<InferValue<Prop, Desc>> :
Desc extends { writable: true } ? InferValue<Prop, Desc> :
Readonly<InferValue<Prop, Desc>>
首先,我们处理writeable 属性的PropertyDescriptor 。这是一组条件,用来定义一些边缘情况和原始属性描述符如何工作的条件。
- 如果我们设置
writable和任何属性访问器(get, set),我们就会失败。never告诉我们,一个错误被抛出。 - 如果我们将
writable设置为false,那么该属性就是只读的。我们推迟到InferValue帮助器类型。 - 如果我们把
writable设置为true,这个属性就不是只读的。我们也会延迟 - 最后,默认情况与
writeable: false相同,所以Readonly<InferValue<Prop, Desc>>。(Readonly<T>是内置的)
这是InferValue 的辅助类型,处理设置value 属性。
type InferValue<Prop extends PropertyKey, Desc> =
Desc extends { get(): any, value: any } ? never :
Desc extends { value: infer T } ? Record<Prop, T> :
Desc extends { get(): infer T } ? Record<Prop, T> : never;
同样是一组条件。
- 如果我们有一个getter和一个set的值,
Object.defineProperty会抛出一个错误,所以永远不会。 - 如果我们已经设置了一个值,让我们推断这个值的类型,用我们定义的属性键和值的类型创建一个对象
- 或者我们从一个getter的返回类型来推断类型。
- 其他的:我们忘了。TypeScript不会让我们对对象进行处理,因为它正在变成
never
在行动中
很多辅助类型,但大概有20行代码就可以搞定了。
type InferValue<Prop extends PropertyKey, Desc> =
Desc extends { get(): any, value: any } ? never :
Desc extends { value: infer T } ? Record<Prop, T> :
Desc extends { get(): infer T } ? Record<Prop, T> : never;
type DefineProperty<
Prop extends PropertyKey,
Desc extends PropertyDescriptor> =
Desc extends { writable: any, set(val: any): any } ? never :
Desc extends { writable: any, get(): any } ? never :
Desc extends { writable: false } ? Readonly<InferValue<Prop, Desc>> :
Desc extends { writable: true } ? InferValue<Prop, Desc> :
Readonly<InferValue<Prop, Desc>>
function defineProperty<
Obj extends object,
Key extends PropertyKey,
PDesc extends PropertyDescriptor>
(obj: Obj, prop: Key, val: PDesc):
asserts obj is Obj & DefineProperty<Key, PDesc> {
Object.defineProperty(obj, prop, val)
}
让我们看看TypeScript是怎么做的。
const storage = {
currentValue: 0
}
defineProperty(storage, 'maxValue', {
writable: false, value: 9001
})
storage.maxValue // it's a number
storage.maxValue = 2 // Error! It's read-only
const storageName = 'My Storage'
defineProperty(storage, 'name', {
get() {
return storageName
}
})
storage.name // it's a string!
// it's not possible to assing a value and a getter
defineProperty(storage, 'broken', {
get() {
return storageName
},
value: 4000
})
// storage is never because we have a malicious
// property descriptor
storage
如前所述,这很可能不会处理所有的边缘情况,但这是一个好的开始。如果你知道你在处理什么,你可以走得很远。