扩展 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:

Typescript Playground

好的,你有一个包含字典的对象。该对象有一个方法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"); 

Playground Link

实施:

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);
}

Link

如果我们不包括那个 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

Link

您可以使用允许您显式声明密钥类型的函数。这里泛型描述的是键而不是对象,所以为了清楚起见,我称它为 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'

Link

我将此作为单独的答案发布,因为您已经更改了问题。这个的类型其实很简单。

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.

Typescript Playground Link

我已经解释了在 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));

您可以继承第一个类型参数的类型:

Typescript playground link

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);
})();