Raj
Raj

Reputation: 129

type guards for optional parameters

I have the following fixture file that i have type guarded below. it has few optional properties

fixture file-

 {
      "profiles": [
        {
          "name": "Laakea",
          "phoneNumber": "2033719225",
          "authGroupName": "Drivers"
        },
        {
          "name": "Lkhagvasuren",
          "phoneNumber": "2033719225",
          "authGroupName": "Drivers"
        },
        {
          "name": "Joaquin",
          "phoneNumber": "2033719225"
        }
      ]
    }

type interface-

 export interface Profile {
      name: string;
      authGroupName?: string;
      phoneNumber?: string;
      email?: string;
    }

type guard function-

export function isValidProfiles(profiles: unknown): profiles is Profile[] {
  if (!Array.isArray(profiles)) {
    return false;
  }
  for (let index = 0; index < profiles.length; index += 1) {
    if (typeof profiles[index].name !== 'string') {
      return false;
    }
    if (profiles[index].email) {
      if (typeof profiles[index].email !== 'string') {
        return false;
      }
    }
    if (profiles[index].phoneNumber) {
      if (typeof profiles[index].phoneNumber !== 'string') {
        return false;
      }
    }
    if (profiles[index].authGroupName) {
      if (typeof profiles[index].authGroupName !== 'string') {
        return false;
      }
    }
  }

  return true;
}

i was wondering if i could write it better instead of all these if statements ?

Upvotes: 2

Views: 972

Answers (2)

jcalz
jcalz

Reputation: 328272

Given that you have essentially identical code for the checks against the email, phoneNumber, and authGroupName properties, you can refactor those checks into a single piece of code that gets run multiple times.

For example, you can make an array of the keys (strongly typed via a const assertion so the compiler remembers that the values in them are literally "email", "phoneNumber", and "authGroupName" as opposed to just string), and then use its every() method to return true if and only if the check works for every key, for every member of the profiles array:

function isValidProfiles(profiles: unknown): profiles is Profile[] {
  if (!Array.isArray(profiles)) { return false; }
  const keys = ["email", "phoneNumber", "authGroupName"] as const;
  return profiles.every(p => p && (typeof p === "object") && (typeof p.name === "string") &&
    keys.every(k => typeof p[k] === "undefined" || typeof p[k] === "string")
  );
}

Here the check I'm using is that, for each key k, and for each profile p, typeof p[k] is either "undefined" or "string". This will correctly deal with string properties and missing properties. It will also accept a property whose key is present but whose value is explicitly undefined. That might not be what you consider "optional", but TypeScript does by default... unless you explicitly enable the --exactOptionalPropertyTypes compiler option... which isn't even part of the "standard" --strict suite of compiler features.


Okay, let's test it:

const val = [
  {
    "name": "Laakea",
    "phoneNumber": "2033719225",
    "authGroupName": "Drivers"
  },
  {
    "name": "Lkhagvasuren",
    "phoneNumber": "2033719225",
    "authGroupName": "Drivers"
  },
  {
    "name": "Joaquin",
    "phoneNumber": "2033719225"
  }
];
if (isValidProfiles(val)) {
  console.log(val.map(
    x => x.authGroupName ?? "no-auth").join(",")
  ); // "Drivers,Drivers,no-auth" 
} else {
  console.log("nope")
}

Looks good. The compiler validates val as being a Profile[] and can treat is as such.

Playground link to code

Upvotes: 1

Raj
Raj

Reputation: 129

came up with this -

export function isValidProfiles(profiles: unknown): profiles is Profile[] {
  if (!Array.isArray(profiles)) {
    return false;
  }
  for (let index = 0; index < profiles.length; index += 1) {
    if (
      !(
        typeof profiles[index].name === 'string' &&
        (!profiles[index].phoneNumber || typeof profiles[index].phoneNumber === 'string') &&
        (!profiles[index].email || typeof profiles[index].email === 'string') &&
        (!profiles[index].authGroupName || typeof profiles[index].authGroupName === 'string')
      )
    ) {
      return false;
    }
  }

  return true;
}

this is no way as concrete and powerful as the function written by @jcalz.

Upvotes: 0

Related Questions