Home > Mobile >  How do i explain to typescript that a function checked if a field was defined?
How do i explain to typescript that a function checked if a field was defined?

Time:05-20

EDIT: code playground

This is the actual code checking that the DB entities passed to my function have all the required joins:

export const checkEntitiesAreJoined = <Type>(baseEntity: Type, keysOfEntitiesToCheck: (keyof Type)[]) => {
    keysOfEntitiesToCheck.forEach((key) => {
        if (!baseEntity[key]){
            throw new Error(`
Some tables haven't been joined!
Expected keys:
${keysOfEntitiesToCheck.join(', ')}
Entity keys:
${Object.keys(baseEntity).join(', ')}`
             )
        }
    })
}

It takes an entity and the fields to check and throws an error if any of them are undefined. It's made in this way because it is used on a lot of different entities so explicitly checking field by field would be silly. How I use this and what the problem is:

const entity: {
    relation_1: string | undefined
    relation_2: string | undefined
} = someAssignedValue
checkEntitiesAreJoined(entity, ["relation_1","relation_2"])
doSomething(entity.relation_1) // entity.relation_1 is still string | undefined here

If I explicitly checked entity.relation_1 with something like:

if(!entity.relation_1) throw new Error("error")

TS would realise that entity.relation_1 can't be undefined further down in the code, but the more generic version that I implemented doesn't do this. I know i can use the non-null assertion like entity.relation_1! but we have a eslint rule forbidding that. Is there a clean way of writing a generic function like the one I want and telling TS that the fields that were passed as arguments cannot be null/undefined? I was thinking something along the lines of returning an actual value with a transformed type but I don't know how to tell TS that this return value has the same type as the baseEntity input argument but that the fields that match the values of keysOfEntitiesToCheck can no longer be falsy. I'm also open to a different approach to checking this that would achieve the same goals.

Take note, I know how to do this when I know the format of the entity being passed into checkEntitiesAreJoined, I'm looking for a way to do this generically i.e. without having to know what keys baseEntity can have. I also can't return Required because other keys that weren't checked might still be falsy.

CodePudding user response:

Assertions are actually possible in TypeScript with the use of asserts before a type guard:

function checkEntitiesAreJoined
    <Type, Keys extends (keyof Type)[]>
    (baseEntity: Type, keysOfEntitiesToCheck: Keys):
    asserts baseEntity is Omit<Type, Keys[number]> & {
        [K in Keys[number]]-?: Exclude<Type[K], undefined>
    } {
    // ...
}

You'll notice that checkEntitiesAreJoined is now a named function (which is required), and that it looks like a type guard but has asserts in front of the usual X is Y.

checkEntitiesAreJoined(entity, ['example_relation'])
// from this point on, 'entity' is Omit<ExampleEntityType, "example_relation"> & { example_relation: string } 

// not affected
entity.something_else

// and it works
return entity.example_relation // string

A cool little playground demonstrating this

  • Related