Home > Enterprise >  Typescript - What is the best way to type check an object properties and throw upon null?
Typescript - What is the best way to type check an object properties and throw upon null?

Time:08-05

Suppose that we have this Typescript code, what could be the best way to code the TODO part ? My team and I are looking for an elegant way to do that

type MyType = {
  key1?: string | null
  key2?: string | null
  key3?: string | null
  // ...
  // ...
  // ...
};

type MyTypeNotNull = {
  key1?: string
  key2?: string
  key3?: string
  // ...
  // ...
  // ...
};

function logMyType(type: MyTypeNotNull) {
  console.log(type);
}

const myType: MyType = {
  key1: 'myKey1',
  // key2 is omitted
  key3: null,
  // ...
  // ...
  // ...
};

const run = (value: MyType) => {
  /* TODO
    - throw when a prop is null
    - make myType pass the type check of logMyType
  */

  logMyType(value); // Error Type 'string | null | undefined' is not assignable to type 'string | undefined'
};

run(myType);

Currently we have a bunch of if following each other, and feel that it should exist a better way to do that, do you have any clean suggestions ?

CodePudding user response:

You could write a custom assertion function that iterates over all its argument's properties and throws if any of them are null; and the returned assertion predicate (of the form asserts xxx is YYY) would narrow the type of the argument to a version with all non-null properties. Here's one approach:

function assertNoPropsAreNull<T extends object>(obj: T): asserts obj is
    { [K in keyof T]: Exclude<T[K], null> } {
    Object.entries(obj).forEach(([k, v]) => {
        if (v === null)
            throw new Error("OH NOEZ, PROP \""   k   "\" IS NULL");
    });
}

The implementation of assertNoPropsAreNull() uses the Object.entries() method to get an array of key/value pairs for the arg argument, and throws an error if it finds any where the value is null (and mentions the key in the error message).

The call signature of assertNoPropsAreNull() is generic in T, the type of obj, and the return type is asserts obj is { [K in keyof T]: Exclude<T[K], null> }. That's a mapped type which just strips null out of each property type via the Exclude<T, U> utility type.

Let's test it:

const run = (value: MyType) => {

  value; // (parameter) value: MyType

  assertNoPropsAreNull(value)
  value; /* (parameter) value: {
    key1?: string | undefined;
    key2?: string | undefined;
    key3?: string | undefined;
  } */

  logMyType(value); // okay    
};

The compiler behavior looks good. Before calling assertNoPropsAreNull(value), the type of value is MyType, and afterwards it has been narrowed to {key1?: string, key2?: string, key3?: string}. This type is structurally identical to your MyTypeNotNull type, and thus logMyType() accepts it with no warnings.

And let's make sure the runtime behavior is also acceptable:

run({ key1: 'myKey1' }); // {key1: "myKey1"}

run({ key1: 'myKey1', key3: null }); //            
  • Related