In typescript, use compile time

Ran Wahle
Israeli Tech Radar
3 min readJul 21, 2024

--

A little story about runtime validation with typescript.

It is a story about how the typescript compiler helped me with a validation feature I wrote after I looked for the wrong solution, realized that I was wrong, and came back from the path I want you all to prevent going through.

The validation task

Let’s look at some simple validation task, to ensure that the object has values for all its fields.

What can you say about this code snippet?

function isObjectValid<TObject)(obj: TObject) {

return Object.values(obj).every(value => Boolean(value) ||
value === 0 or value === false));

}

Sound simple, right? Would it return false for this code?

type ObjType = {prop: string, prop2?: string};

obj: ObjType {prop: 'propValue'}

console.log(isObjectValid(obj));

Well, it would return true because the Object.values() function won’t iterate through the value of ‘prop2’.
Yes, I know, prop2 is optional here, but this is only to simulate a situation that probably will take place in runtime, because after all, it’s javascript at run time.

Why not use generics?

Generics are great! I can tell the function something about the type it runs on, there is a way of pulling all keys to an array, and make isObjectValid function would know what are the keys of the type, you may read all about it here, keep in mind that the solution there is not CPU friendly.

But I wanted to keep my CPU happy so I’d stick to writing keys.

const keys = ['key1', 'key2'] as const;
type MyObjectType = Record<TypeFromKeysArray, unknown>

const obj: MyObjectType = {
key1: 'val1',
key2: 'val2'
}


function isObjectValid<T>(obj: T, keys: (keyof T)[]) {
return keys.every(key => Boolean(obj[key]) || obj[key] === 0 || obj[key] === false )
}

// The usage of keys.map(key => key) is for the parameter types to be casted,
// Otherwise we'll get an incompatible type error
console.log(isObjectValid(obj, keys.map(key => key))) // Produces true

OK, now, let’s simulate a situation when an object is not full and should be invalid.

const obj2: MyObjectType = {
key1: 'val1',
key2: 'val2'
}

const unknownObject2 = obj2 as {key1: string; key2?: string};

// This is to simulate the situation, but we could get an empty field
// at runtime
delete unknownObject2['key2']

console.log(isObjectValid(obj2, keys.map(key => key))) // produces false

The code above simulates a scenario when although typescript requires all fields, the object is received while some of its fields are missing. This can happen on the runtime.

Let’s see an example with regular types or interfaces

In case we’re using a regular interface, we can write the keys as an array of strings.

interface AnInterface {
prop1: string;
prop2: number;
}

const interfaceObj: AnInterface = {
prop1: 'prop1Value',
prop2: 2
}

console.log(isObjectValid(interfaceObj, ['prop1', 'prop2']))

It is almost needless to say, but you be alarmed by the string-like format. The keys will be shown in every IDE in an autocomplete list and the typescript compiler won’t let us put keys other than the ones on the type/interface.

In conclusion

Validating that an object has all its required fields filled is one of the most common validation task I’ve encountered, and typescript compiler actually helped me in doing it easily and in a relatively safe manner.

You may find this approach a little error prune because I can easily forget some fields, this can be overcome by testing these calls, but even without that, typescript enables us to validate only the fields we wish to, and makes sure we won’t validate fields that aren’t in our object.

--

--