扩展 Typescript 索引对象以一般访问索引对象(字典)的值的类型
Extending Typescript indexed objects to generically access the type of the value of the indexed object (dictionary)
我想在 Typescript 中扩展默认的索引匿名类型,但似乎找不到正确的语法来获取索引对象值的类型,如果这完全可能的话?
问题是:
编辑:
我修改了示例以更好地解释问题。
// How do we pass the value for T as a dictionary through to the iterator?.
interface Object {
where<T = { [key: string]: any}, K = keyof T>(this: T, iterator: (v: any /* this should be valueof T, not any */, key?: K) => boolean | void): T;
// This works, but you have to specify the type when calling it, this is exactly what I'm trying to avoid.
whereAsArray<S, T = { [key: string]: S }, K = keyof T>(this: T, iterator: (v: S /* this should be valueof T, not S*/, key?: K) => boolean | void): S[] /* this should be valueof T[], not S */;
}
// This problem is agnostic of the implementation. Included here only so that the code runs.
(function Setup() {
if (typeof Object.prototype.where != 'function') { Object.defineProperty(Object.prototype, 'where', { value: function (iterator: (v: any, key?: string) => any) { const keys: string[] = Object.keys(this); const result: any = {}; let i: number; for (i = 0; i < keys.length; i++) { const key = keys[i]; if (this.hasOwnProperty(key)) { const res = iterator(this[key], key); if (res === true) { result[key] = (this[key]); } } } return result; }, writable: true, configurable: true, enumerable: false }); }
if (typeof Object.prototype.whereAsArray != 'function') { Object.defineProperty(Object.prototype, 'whereAsArray', { value: function (iterator: (v: any, key?: string) => any) { const keys: string[] = Object.keys(this); const result: any[] = []; let i: number; for (i = 0; i < keys.length; i++) { const key = keys[i]; if (this.hasOwnProperty(key)) { const res = iterator(this[key], key); if (res === true) { result.push(this[key]); } } } return result; }, writable: true, configurable: true, enumerable: false }); }
})();
(function Test() {
// Typescript knows the type of x, it is explicitly typed here as an object keyed on string, storing only numbers; a dictionary of string=>number.
// Typescript enforces this everywhere you use this dictionary, as shown below:
const x: { [key: string]: number } = {};
x["foo"] = 1;
x["bar"] = 2;
// The code can currently be used like this, and works.. But, if you hover the 'i' variable, you will see that the type is any,
// because I can't figure out how to extract the type of the "value of" T in the interface?
const results = x.where((i, _k) => i !== 1);
console.log(results);
// This works. But we have to specify <number> for TS to figure this out. Question is, is it possible to have TS figure this out from the signature?
// Having to type the function calls should not be necessary.
const results2 = x.whereAsArray<number>((i, _k) => i === 1);
console.log(results2);
})();
游乐场link:
好的,你有一个包含字典的对象。该对象有一个方法dictionaryKeys()
,它return是字典键的数组,还有一个方法dictionaryWhere()
,它根据键和值进行过滤,我猜return是一个子集字典的?
您可以使用打字稿 Record
utility type,但您缺少的重要一点是泛型 T
应该应用于对象,而不是个别方法。
interface DictionaryObject<T> {
dictionaryKeys(): string[];
dictionaryWhere(iterator: (v: T, key: string) => boolean): Record<string, T>;
}
declare const x: DictionaryObject<string>
let k: string[] = x.dictionaryKeys();
let v: Record<string, string> = x.dictionaryWhere((i, k) => k === "foo");
实施:
class MyDictionary<T> {
private _values: Record<string, T> = {};
constructor(object: Record<string, T> = {}) {
this._values = object;
}
dictionaryKeys(): string[] {
return Object.keys(this);
}
dictionaryWhere(iterator: (v: T, key: string) => boolean): Record<string, T> {
return Object.fromEntries(
Object.entries(this._values).filter(
([key, value]) => iterator(value, key)
)
)
}
}
注意:tsconfig至少需要es2017才能使用Object.fromEntries
.
编辑:
我误解了这个问题,没有意识到您正在尝试扩展 built-in Object.prototype
。这是一个坏主意,因为它会产生意想不到的后果。在我看来,您应该创建接受对象而不是扩展原型的函数。
并非所有对象都是 string-keyed 字典。数组是一种对象。这就是为什么您获得键的类型 (string | number)[]
的原因。作为explained in the docs,
keyof and T[K] interact with index signatures. An index signature
parameter type must be ‘string’ or ‘number’. If you have a type with
a string index signature, keyof T will be string | number (and not
just string, since in JavaScript you can access an object property
either by using strings (object["42"]) or numbers (object[42])).
如果您已经必须使用 let k = x.dictionaryKeys<string>();
传递显式泛型,我真的不明白为什么这比 let k = Object.keys(x) as string[];
.
您可以使用基于对象键入键数组的函数,由于 .
中的原因,您需要谨慎使用该函数
const typedKeys = <T extends Record<string, any> & Record<number, never>>(obj: T): (keyof T)[] => {
return Object.keys(obj);
}
如果我们不包括那个 Record<number, never>
,我们实际上 可以 仍然传递数字键控对象(由于事情在幕后实现的方式),我们会得到不好的结果。
const typedKeys = <T extends Record<string, any>>(obj: T): (keyof T)[] => {
return Object.keys(obj);
}
const a = {
1: "",
2: "",
};
const b = {
one: "",
two: "",
};
const aKeys = typedKeys(a); // we might expect a problem passing in an object with numeric keys, but there is no error
console.log( aKeys ); // BEWARE: typescript says the type is `(1|2)[]`, but the values are `["1", "2"]`
const bKeys = typedKeys(b);
console.log( bKeys ); // this one is fine
您可以使用允许您显式声明密钥类型的函数。这里泛型描述的是键而不是对象,所以为了清楚起见,我称它为 K
。我们使用 as K[]
将类型缩小为 string
的子集。由于 Object.keys
returns 字符串,我们不能 return 任何不能分配给 string[]
.
的东西
const typedKeys = <K extends string>(obj: Record<K, any> & Record<number | symbol, never>): K[] => {
return Object.keys(obj) as K[];
}
const b = {
one: "",
two: "",
};
const bKeys = typedKeys(b); // type is ("one" | "two")[], with no generic, the type is infered to these specific keys
const stringKeys = typedKeys<string>(b); // type is string[]
const badKeys = typedKeys<number>(b); // error type 'number' does not satisfy the constraint 'string'
我将此作为单独的答案发布,因为您已经更改了问题。这个的类型其实很简单。
type ValueOf<T> = T[keyof T];
interface Object {
where<T>(this: T, iterator: (v: ValueOf<T>, key: string) => boolean | void): T; // should become Partial<T> if we are not dealing with a dictionary
whereAsArray<T>(this: T, iterator: (v: ValueOf<T>, key: string) => boolean | void): ValueOf<T>[];
}
我们不需要也不想要通用的键,因为这些函数适用于所有键,而不是特定键。
请注意,当定义 iterator
回调时,应按要求列出附加参数。当您 调用 带有回调的 where
方法时,您不需要包含所有参数。但是,如果您在回调定义中将它们设为可选,您将无法在回调主体中实际使用它们,因为它们可能是 undefined
.
我已经解释了在 Object.prototype
上定义这些方法的许多潜在问题,但我确实想提请您注意一个问题。我们的 where
returns 与原始对象相同类型 T
。这本质上不是问题,但它与您的实现不匹配,因为数组作为对象而不是数组返回。然而,Typescript 期望返回一个数组,从而导致运行时错误。
const z = [1, 2, 3];
const filtered: number[] = z.where(v => v > 1);
// the type is number[] but it's actually an object
console.log(filtered);
// so calling array methods on it seems fine to TS, but is a runtime error
console.log(filtered.map(n => n - 1));
您可以继承第一个类型参数的类型:
interface Object {
whereAsArray <T, K = keyof T, V = T[keyof T] > (this: T, iterator: (v: V, key?: K) => boolean | void): T[];
}
(function Setup() {
if (typeof Object.prototype.whereAsArray != 'function') {
Object.defineProperty(Object.prototype, 'whereAsArray', {
value: function(iterator: (v: any, key ? : string) => any) {
const keys: string[] = Object.keys(this);
const result: any[] = [];
let i: number;
for (i = 0; i < keys.length; i++) {
const key = keys[i];
if (this.hasOwnProperty(key)) {
const res = iterator(this[key], key);
if (res === true) {
result.push(this[key]);
}
}
}
return result;
},
writable: true,
configurable: true,
enumerable: false
});
}
})();
(function Test() {
const x: {
[key: string]: number
} = {};
x["foo"] = 1;
x["bar"] = 2;
const results = x.whereAsArray((i, _k) => i === 1);
console.log(results);
})();
我想在 Typescript 中扩展默认的索引匿名类型,但似乎找不到正确的语法来获取索引对象值的类型,如果这完全可能的话?
问题是:
编辑:
我修改了示例以更好地解释问题。
// How do we pass the value for T as a dictionary through to the iterator?.
interface Object {
where<T = { [key: string]: any}, K = keyof T>(this: T, iterator: (v: any /* this should be valueof T, not any */, key?: K) => boolean | void): T;
// This works, but you have to specify the type when calling it, this is exactly what I'm trying to avoid.
whereAsArray<S, T = { [key: string]: S }, K = keyof T>(this: T, iterator: (v: S /* this should be valueof T, not S*/, key?: K) => boolean | void): S[] /* this should be valueof T[], not S */;
}
// This problem is agnostic of the implementation. Included here only so that the code runs.
(function Setup() {
if (typeof Object.prototype.where != 'function') { Object.defineProperty(Object.prototype, 'where', { value: function (iterator: (v: any, key?: string) => any) { const keys: string[] = Object.keys(this); const result: any = {}; let i: number; for (i = 0; i < keys.length; i++) { const key = keys[i]; if (this.hasOwnProperty(key)) { const res = iterator(this[key], key); if (res === true) { result[key] = (this[key]); } } } return result; }, writable: true, configurable: true, enumerable: false }); }
if (typeof Object.prototype.whereAsArray != 'function') { Object.defineProperty(Object.prototype, 'whereAsArray', { value: function (iterator: (v: any, key?: string) => any) { const keys: string[] = Object.keys(this); const result: any[] = []; let i: number; for (i = 0; i < keys.length; i++) { const key = keys[i]; if (this.hasOwnProperty(key)) { const res = iterator(this[key], key); if (res === true) { result.push(this[key]); } } } return result; }, writable: true, configurable: true, enumerable: false }); }
})();
(function Test() {
// Typescript knows the type of x, it is explicitly typed here as an object keyed on string, storing only numbers; a dictionary of string=>number.
// Typescript enforces this everywhere you use this dictionary, as shown below:
const x: { [key: string]: number } = {};
x["foo"] = 1;
x["bar"] = 2;
// The code can currently be used like this, and works.. But, if you hover the 'i' variable, you will see that the type is any,
// because I can't figure out how to extract the type of the "value of" T in the interface?
const results = x.where((i, _k) => i !== 1);
console.log(results);
// This works. But we have to specify <number> for TS to figure this out. Question is, is it possible to have TS figure this out from the signature?
// Having to type the function calls should not be necessary.
const results2 = x.whereAsArray<number>((i, _k) => i === 1);
console.log(results2);
})();
游乐场link:
好的,你有一个包含字典的对象。该对象有一个方法dictionaryKeys()
,它return是字典键的数组,还有一个方法dictionaryWhere()
,它根据键和值进行过滤,我猜return是一个子集字典的?
您可以使用打字稿 Record
utility type,但您缺少的重要一点是泛型 T
应该应用于对象,而不是个别方法。
interface DictionaryObject<T> {
dictionaryKeys(): string[];
dictionaryWhere(iterator: (v: T, key: string) => boolean): Record<string, T>;
}
declare const x: DictionaryObject<string>
let k: string[] = x.dictionaryKeys();
let v: Record<string, string> = x.dictionaryWhere((i, k) => k === "foo");
实施:
class MyDictionary<T> {
private _values: Record<string, T> = {};
constructor(object: Record<string, T> = {}) {
this._values = object;
}
dictionaryKeys(): string[] {
return Object.keys(this);
}
dictionaryWhere(iterator: (v: T, key: string) => boolean): Record<string, T> {
return Object.fromEntries(
Object.entries(this._values).filter(
([key, value]) => iterator(value, key)
)
)
}
}
注意:tsconfig至少需要es2017才能使用Object.fromEntries
.
编辑:
我误解了这个问题,没有意识到您正在尝试扩展 built-in Object.prototype
。这是一个坏主意,因为它会产生意想不到的后果。在我看来,您应该创建接受对象而不是扩展原型的函数。
并非所有对象都是 string-keyed 字典。数组是一种对象。这就是为什么您获得键的类型 (string | number)[]
的原因。作为explained in the docs,
keyof and T[K] interact with index signatures. An index signature parameter type must be ‘string’ or ‘number’. If you have a type with a string index signature, keyof T will be string | number (and not just string, since in JavaScript you can access an object property either by using strings (object["42"]) or numbers (object[42])).
如果您已经必须使用 let k = x.dictionaryKeys<string>();
传递显式泛型,我真的不明白为什么这比 let k = Object.keys(x) as string[];
.
您可以使用基于对象键入键数组的函数,由于
const typedKeys = <T extends Record<string, any> & Record<number, never>>(obj: T): (keyof T)[] => {
return Object.keys(obj);
}
如果我们不包括那个 Record<number, never>
,我们实际上 可以 仍然传递数字键控对象(由于事情在幕后实现的方式),我们会得到不好的结果。
const typedKeys = <T extends Record<string, any>>(obj: T): (keyof T)[] => {
return Object.keys(obj);
}
const a = {
1: "",
2: "",
};
const b = {
one: "",
two: "",
};
const aKeys = typedKeys(a); // we might expect a problem passing in an object with numeric keys, but there is no error
console.log( aKeys ); // BEWARE: typescript says the type is `(1|2)[]`, but the values are `["1", "2"]`
const bKeys = typedKeys(b);
console.log( bKeys ); // this one is fine
您可以使用允许您显式声明密钥类型的函数。这里泛型描述的是键而不是对象,所以为了清楚起见,我称它为 K
。我们使用 as K[]
将类型缩小为 string
的子集。由于 Object.keys
returns 字符串,我们不能 return 任何不能分配给 string[]
.
const typedKeys = <K extends string>(obj: Record<K, any> & Record<number | symbol, never>): K[] => {
return Object.keys(obj) as K[];
}
const b = {
one: "",
two: "",
};
const bKeys = typedKeys(b); // type is ("one" | "two")[], with no generic, the type is infered to these specific keys
const stringKeys = typedKeys<string>(b); // type is string[]
const badKeys = typedKeys<number>(b); // error type 'number' does not satisfy the constraint 'string'
我将此作为单独的答案发布,因为您已经更改了问题。这个的类型其实很简单。
type ValueOf<T> = T[keyof T];
interface Object {
where<T>(this: T, iterator: (v: ValueOf<T>, key: string) => boolean | void): T; // should become Partial<T> if we are not dealing with a dictionary
whereAsArray<T>(this: T, iterator: (v: ValueOf<T>, key: string) => boolean | void): ValueOf<T>[];
}
我们不需要也不想要通用的键,因为这些函数适用于所有键,而不是特定键。
请注意,当定义 iterator
回调时,应按要求列出附加参数。当您 调用 带有回调的 where
方法时,您不需要包含所有参数。但是,如果您在回调定义中将它们设为可选,您将无法在回调主体中实际使用它们,因为它们可能是 undefined
.
我已经解释了在 Object.prototype
上定义这些方法的许多潜在问题,但我确实想提请您注意一个问题。我们的 where
returns 与原始对象相同类型 T
。这本质上不是问题,但它与您的实现不匹配,因为数组作为对象而不是数组返回。然而,Typescript 期望返回一个数组,从而导致运行时错误。
const z = [1, 2, 3];
const filtered: number[] = z.where(v => v > 1);
// the type is number[] but it's actually an object
console.log(filtered);
// so calling array methods on it seems fine to TS, but is a runtime error
console.log(filtered.map(n => n - 1));
您可以继承第一个类型参数的类型:
interface Object {
whereAsArray <T, K = keyof T, V = T[keyof T] > (this: T, iterator: (v: V, key?: K) => boolean | void): T[];
}
(function Setup() {
if (typeof Object.prototype.whereAsArray != 'function') {
Object.defineProperty(Object.prototype, 'whereAsArray', {
value: function(iterator: (v: any, key ? : string) => any) {
const keys: string[] = Object.keys(this);
const result: any[] = [];
let i: number;
for (i = 0; i < keys.length; i++) {
const key = keys[i];
if (this.hasOwnProperty(key)) {
const res = iterator(this[key], key);
if (res === true) {
result.push(this[key]);
}
}
}
return result;
},
writable: true,
configurable: true,
enumerable: false
});
}
})();
(function Test() {
const x: {
[key: string]: number
} = {};
x["foo"] = 1;
x["bar"] = 2;
const results = x.whereAsArray((i, _k) => i === 1);
console.log(results);
})();