The definition files for the TypeScript typings related to the static method Object.entries()
are as follows:
interface ObjectConstructor {
entries<T>(o: { [s: string]: T } | ArrayLike<T>): [string, T][];
entries(o: {}): [string, any][];
}
Both of these interfaces return an array of key-value pairs where the key is always a type of string
. The reason behind not getting K
lies in the fact that TypeScript object types are not "sealed" or "exact," allowing objects to have unspecified properties beyond TypeScript's knowledge. This behavior can be likened to Object.keys() returning a list of strings instead of keyof types.
Consider this example to understand how excess properties work:
interface Foo {
bar: string,
baz: string;
}
const x = { bar: "abc", baz: "def", qux: 123 };
const y: Foo = x; // this assignment works fine
In the above snippet, assigning const y: Foo = x
is valid because all required properties of Foo
exist within x
, and extra properties do not invalidate the assignment. While there are checks on object literals for excessive properties, like
const y: Foo = { bar: "abc", baz: "def", qux: 123 }
, failures occur when information about
qux
gets lost during compilation.
This is why your filterProps()
method might be deemed unsafe, enabling scenarios like:
filterProps(y, (k, v) => v.toUpperCase() === v) // compiles without errors but leads to runtime issues
// 💥 RUNTIME ERROR! v.toUpperCase is not a function
Here, v
being treated as a string
causes runtime breakdowns due to incompatible types. While such calls compile error-free due to their signature compliance, they falter at execution time due to mismatching data types.
To assert that your object lacks unwanted properties, you can use type assertions with statements like Object.entries(object)
ensure returned values match expected types:
function filterProps<K extends string | symbol | number, V>(
object: Record<K, V>, fn: (key: K, value: V, object: Record<K, V>) => unknown) {
return (Object.entries(object) as Array<[K, V]>).reduce(
// --------------------------> ^^^^^^^^^^^^^^^^
(acum, [key, value]) => fn(key, value, object) ? { ...acum, [key]: value } : acum,
{} as Record<K, V>
)
}
This assertion enforces compiler trust in your validations regarding output types from functions like Object.entries()
, transferring careful handling responsibilities to you rather than the compiler itself. Proceed cautiously when implementing such solutions!
Access the playground link for the code here