In an attempt to create a user-defined type guard function for a specific use-case, I am faced with a challenge:
- There are over 100 TypeScript functions, each requiring an
options
object. - These functions utilize only certain properties from the object while disregarding the others.
- The type of the
options
object allows for null values, but if any necessary properties are null, the function should log the missing property name to the console and exit immediately.
My concept is to combine input validation, logging of invalid inputs, and a type guard into a single function that can accept an object. If any properties within the object are null or undefined, it will log the null property names to the console. The function should return true if all properties have non-null/non-undefined values, and false if any are null or undefined. Furthermore, upon returning, it should act as a type guard so that the object's properties can be referenced without having to cast them to non-nullable types.
Here's my initial approach:
type AllNonNullable<T> = { [P in keyof T]: NonNullable<T[P]> };
type StringKeyedObject = { [s: string]: any };
const allPropertiesHaveValuesLogged = <T extends StringKeyedObject>(values: T) : values is AllNonNullable<T> => {
for (const key in Object.keys(values)) {
if (values[key] == null) {
console.log(`${key} is missing`);
return false;
}
}
return true;
}
I envisioned utilizing this function in a simple example like this:
interface Foo {
prop1: string | null;
prop2: number | null;
prop3: {} | null;
}
const test1 = (foo: Foo): boolean => {
if (!allPropertiesHaveValuesLogged(foo)) {
return false;
}
const { prop1, prop2 } = foo;
console.log(`${prop1.toLowerCase()} and then ${prop2.toFixed(0)}`);
return true;
}
However, a major issue arises where all properties of foo
are being checked instead of just the two properties used by the code. Some other properties may actually be allowed to be null, but the focus is specifically on prop1
and prop2
.
In my subsequent attempt, I resorted to a verbose solution like this:
const test2 = (foo: Foo): boolean => {
const propsToUse = { prop1: foo.prop1, prop2: foo.prop2 };
if (!allPropertiesHaveValuesLogged(propsToUse)) {
return false;
}
const {prop1, prop2} = propsToUse;
console.log(`${prop1.toLowerCase()} and then ${prop2.toFixed(0)}`);
return true;
}
Unfortunately, this method requires typing each property name multiple times and could lead to difficulties when renaming properties.
Finally, I came up with what I believe to be the most concise and least repetitive solution. However, TypeScript does not recognize that my type guard should apply to prop1
and
prop2</code. This might be due to the type guard being applied only to the anonymous object created during the function call.</p>
<pre><code>const test3 = (foo: Foo): boolean => {
const {prop1, prop2} = foo;
if (!allPropertiesHaveValuesLogged({prop1, prop2})) {
return false;
}
console.log(`${prop1.toLowerCase()} and then ${prop2.toFixed(0)}`);
return true;
}
Hence, #1 poses a runtime bug, #2 is cumbersome and prone to errors, and #3 results in compile errors which might get resolved in future TypeScript releases.
Is there a better solution that would work seamlessly on TypeScript 3.0? Perhaps even on 3.1?