获取通用对象成员的编译时类型
Get compile time type of member of Generic'd object
我正在尝试创建一个函数来简化可用于任意目的的对象上的 set
ter 函数的设置。明显的用途包括类型保护、值和绑定检查或触发事件。
我们的想法是采用一个已知形状的对象并调用一个函数 return 一个具有相同可枚举形状的新对象,其中每个对象的成员实际上都具有 set
和 get
"Properties"。 set
ters 来自函数的第二个参数,get
ters 只是 return "protected" 值。
此方法的大量实用性来自可能的严格类型。当受保护对象在您甚至可能无法控制的单独代码段中定义时,这尤其有用。如果对象形状发生变化,类型错误将确保新的 set
ter 为 added/removed.
我很容易地创建了一个 "flat" 版本。
扁平化版本
function makeObjectSetter<T extends {}>(
internal: T,
setters: {
[P in keyof T]: (next: T[P]) => void;
}
) {
const ret = {};
for (const x in internal) {
Object.defineProperty(ret, x, {
set: setters[x],
get: () => internal[x],
enumerable: true,
});
}
return ret as T;
}
用法
const myObject = {
num: 42,
str: 'initialValue',
};
const protectedObject = makeObjectSetter(myObject, {
num(x) {
// Make sure positive
myObject.num = Math.max(x, 0);
},
str(s) {
// Always double the input
myObject.str = s + s;
},
});
console.log(myObject);
// { num: 42, str: 'initialValue' }
protectedObject.num = -1;
protectedObject.str = 'a';
console.log(myObject);
// { num: 0, str: 'aa' }
for (let x in protectedObject) console.log(x);
// num
// str
当目标对象具有嵌套对象时,棘手的部分就来了。虽然可以使用扁平化版本,但是嵌套越深写起来越麻烦。
所以,我正在尝试编写平面函数的递归版本。这会检测成员的类型是否为对象并以不同方式对待。我相信我的函数签名类型是正确的,但是函数内部有一个我无法弄清楚的单行硬错误。
我知道 TypeScript 类型在运行时不可用,但我相信这是一个编译时问题。我相信如果我在指定的行中获得正确的语法,它就会起作用。但也许我错了?
递归版本
type NestedSetters<T extends {}> = { [P in keyof T]: T[P] extends {} ? NestedSetters<T[P]> : (next: T[P]) => void };
function makeObjectSetterRecursive<T extends {}>(internal: T, setters: NestedSetters<T>) {
const ret = {};
for (const x in internal) {
let prop: PropertyDescriptor;
// Can't figure out this line
type t = typeof internal[x];
// Pretty sure this test is the right runtime test for my purposes
if (typeof internal[x] == 'object') {
prop = {
value: makeObjectSetterRecursive(internal[x], setters[x] as NestedSetters<t>), // Should be able to avoid this `as` cast, no?
};
} else {
prop = {
set: setters[x] as (next: t) => void, // Should be able to avoid this `as` cast, no?
get: () => internal[x],
};
}
prop.enumerable = true;
Object.defineProperty(ret, x, prop);
}
return ret as T; // Extra extra bonus get rid of this `as` cast
}
除了typeof internal[x]
,我也试过Pick<typeof internal, x>
和其他猜测都无济于事。
如有任何想法,我们将不胜感激。答案可能是我想要的不可能。
支线任务:我觉得 as
类型转换不需要正确的类型提示。
如您所述,TypeScript 类型在运行时不可用,因此 typeof internal[x]
无法工作。您正在寻找的是 T[Extract<keyof T, string>]
,它提取了道具值的类型。
关于转换问题,条件类型缩小似乎存在问题。
https://github.com/microsoft/TypeScript/issues/30152
因此,必须依赖 if else 语句中映射正确类型的运行时逻辑(在本例中为 typeof value === 'object'
)。对于语义,我认为创建用户定义的类型保护 isNestedSetters
并将值转换为联合类型 NestedSetters<T[P]> | Setter<T[P]>
很有用,因为这样编译器可以正确缩小范围。
完整版如下:
type Setter<T> = T extends boolean ? (next: boolean) => void : (next: T) => void
type SetterOrNested<T> = T extends object ? NestedSetters<T> : Setter<T>
type NestedSetters<T> = {
[P in keyof T]: SetterOrNested<T[P]>
}
function isNestedSetters<T>(value: any): value is NestedSetters<T> {
return typeof value === 'object';
}
function makeObjectSetterRecursive<T extends {}>(internal: T, setters: NestedSetters<T>) {
const ret = <T>{};
for (const x in internal) {
let prop: PropertyDescriptor;
type P = Extract<keyof T, string>
const setterOrNested = setters[x] as NestedSetters<T[P]> | Setter<T[P]>
if (isNestedSetters<T[P]>(setterOrNested)) {
prop = {
value: makeObjectSetterRecursive(internal[x], setterOrNested),
};
} else {
prop = {
set: setterOrNested,
get: () => internal[x],
};
}
prop.enumerable = true;
Object.defineProperty(ret, x, prop);
}
return ret;
}
应该输出以下内容:
const myObject = {
num: 42,
str: 'initialValue',
others: {
bool: true,
nestedStr: ''
}
};
const protectedObject = makeObjectSetterRecursive(myObject, {
num(x) {
// Make sure positive
myObject.num = Math.max(x, 0);
},
str(s) {
// Always double the input
myObject.str = s + s;
},
others: {
bool(b) {
// Toggle
myObject.others.bool = !b
},
nestedStr(s) {
// Add 3 dots
myObject.others.nestedStr = s + '...'
}
}
});
console.log(myObject);
// { num: 42, str: 'initialValue', others: { bool: true, nestedStr: '' } }
protectedObject.num = -1;
protectedObject.str = 'a';
console.log(myObject);
// { num: 0, str: 'aa', others: { bool: true, nestedStr: '' } }
protectedObject.others.bool = true;
protectedObject.others.nestedStr = 'abc';
console.log(myObject);
// { num: 0, str: 'aa', others: { bool: false, nestedStr: 'abc...' } }
我必须说,我不太确定它的用例,但这是一个有趣的概念,所以我还是决定试一试。
我正在尝试创建一个函数来简化可用于任意目的的对象上的 set
ter 函数的设置。明显的用途包括类型保护、值和绑定检查或触发事件。
我们的想法是采用一个已知形状的对象并调用一个函数 return 一个具有相同可枚举形状的新对象,其中每个对象的成员实际上都具有 set
和 get
"Properties"。 set
ters 来自函数的第二个参数,get
ters 只是 return "protected" 值。
此方法的大量实用性来自可能的严格类型。当受保护对象在您甚至可能无法控制的单独代码段中定义时,这尤其有用。如果对象形状发生变化,类型错误将确保新的 set
ter 为 added/removed.
我很容易地创建了一个 "flat" 版本。
扁平化版本
function makeObjectSetter<T extends {}>(
internal: T,
setters: {
[P in keyof T]: (next: T[P]) => void;
}
) {
const ret = {};
for (const x in internal) {
Object.defineProperty(ret, x, {
set: setters[x],
get: () => internal[x],
enumerable: true,
});
}
return ret as T;
}
用法
const myObject = {
num: 42,
str: 'initialValue',
};
const protectedObject = makeObjectSetter(myObject, {
num(x) {
// Make sure positive
myObject.num = Math.max(x, 0);
},
str(s) {
// Always double the input
myObject.str = s + s;
},
});
console.log(myObject);
// { num: 42, str: 'initialValue' }
protectedObject.num = -1;
protectedObject.str = 'a';
console.log(myObject);
// { num: 0, str: 'aa' }
for (let x in protectedObject) console.log(x);
// num
// str
当目标对象具有嵌套对象时,棘手的部分就来了。虽然可以使用扁平化版本,但是嵌套越深写起来越麻烦。
所以,我正在尝试编写平面函数的递归版本。这会检测成员的类型是否为对象并以不同方式对待。我相信我的函数签名类型是正确的,但是函数内部有一个我无法弄清楚的单行硬错误。
我知道 TypeScript 类型在运行时不可用,但我相信这是一个编译时问题。我相信如果我在指定的行中获得正确的语法,它就会起作用。但也许我错了?
递归版本
type NestedSetters<T extends {}> = { [P in keyof T]: T[P] extends {} ? NestedSetters<T[P]> : (next: T[P]) => void };
function makeObjectSetterRecursive<T extends {}>(internal: T, setters: NestedSetters<T>) {
const ret = {};
for (const x in internal) {
let prop: PropertyDescriptor;
// Can't figure out this line
type t = typeof internal[x];
// Pretty sure this test is the right runtime test for my purposes
if (typeof internal[x] == 'object') {
prop = {
value: makeObjectSetterRecursive(internal[x], setters[x] as NestedSetters<t>), // Should be able to avoid this `as` cast, no?
};
} else {
prop = {
set: setters[x] as (next: t) => void, // Should be able to avoid this `as` cast, no?
get: () => internal[x],
};
}
prop.enumerable = true;
Object.defineProperty(ret, x, prop);
}
return ret as T; // Extra extra bonus get rid of this `as` cast
}
除了typeof internal[x]
,我也试过Pick<typeof internal, x>
和其他猜测都无济于事。
如有任何想法,我们将不胜感激。答案可能是我想要的不可能。
支线任务:我觉得 as
类型转换不需要正确的类型提示。
如您所述,TypeScript 类型在运行时不可用,因此 typeof internal[x]
无法工作。您正在寻找的是 T[Extract<keyof T, string>]
,它提取了道具值的类型。
关于转换问题,条件类型缩小似乎存在问题。
https://github.com/microsoft/TypeScript/issues/30152
因此,必须依赖 if else 语句中映射正确类型的运行时逻辑(在本例中为 typeof value === 'object'
)。对于语义,我认为创建用户定义的类型保护 isNestedSetters
并将值转换为联合类型 NestedSetters<T[P]> | Setter<T[P]>
很有用,因为这样编译器可以正确缩小范围。
完整版如下:
type Setter<T> = T extends boolean ? (next: boolean) => void : (next: T) => void
type SetterOrNested<T> = T extends object ? NestedSetters<T> : Setter<T>
type NestedSetters<T> = {
[P in keyof T]: SetterOrNested<T[P]>
}
function isNestedSetters<T>(value: any): value is NestedSetters<T> {
return typeof value === 'object';
}
function makeObjectSetterRecursive<T extends {}>(internal: T, setters: NestedSetters<T>) {
const ret = <T>{};
for (const x in internal) {
let prop: PropertyDescriptor;
type P = Extract<keyof T, string>
const setterOrNested = setters[x] as NestedSetters<T[P]> | Setter<T[P]>
if (isNestedSetters<T[P]>(setterOrNested)) {
prop = {
value: makeObjectSetterRecursive(internal[x], setterOrNested),
};
} else {
prop = {
set: setterOrNested,
get: () => internal[x],
};
}
prop.enumerable = true;
Object.defineProperty(ret, x, prop);
}
return ret;
}
应该输出以下内容:
const myObject = {
num: 42,
str: 'initialValue',
others: {
bool: true,
nestedStr: ''
}
};
const protectedObject = makeObjectSetterRecursive(myObject, {
num(x) {
// Make sure positive
myObject.num = Math.max(x, 0);
},
str(s) {
// Always double the input
myObject.str = s + s;
},
others: {
bool(b) {
// Toggle
myObject.others.bool = !b
},
nestedStr(s) {
// Add 3 dots
myObject.others.nestedStr = s + '...'
}
}
});
console.log(myObject);
// { num: 42, str: 'initialValue', others: { bool: true, nestedStr: '' } }
protectedObject.num = -1;
protectedObject.str = 'a';
console.log(myObject);
// { num: 0, str: 'aa', others: { bool: true, nestedStr: '' } }
protectedObject.others.bool = true;
protectedObject.others.nestedStr = 'abc';
console.log(myObject);
// { num: 0, str: 'aa', others: { bool: false, nestedStr: 'abc...' } }
我必须说,我不太确定它的用例,但这是一个有趣的概念,所以我还是决定试一试。