How do you validate properties on an `unknown` typed parameter in typescript?

Viewed 3205

I'm sure there is something super basic here but I can't quite figure it out. This is my first time digging into the unknown type and I'm using it to validate a user configuration file.

I'm aware that you have to basically check values on that unknown type to get typescript to get more and more information about the objects themselves. Here's what I have so far

function validateJsonOverride(override: unknown) {
  if (typeof override !== "object") {
    throw new Error("Property `overrides` must be an array of objects");
  }
  if (override == null) {
    throw new Error("Elements in `overrides` cannot be null or undefined");
  }
  
  if (!("a" in override)) {
    throw new Error("Property `a` is required for json overrides");
  }
  if (!("b" in override)) {
    throw new Error("Property `b` is required for json overrides");
  }
  const {a, b} = override;
  return {
    a,
    b,
  };

However right now at the line const {a, b} = override; I get an error Property 'a' does not exist on type '{}'.ts(2339).

How are you supposed to clarify the types to typescript so that a and b do exist.

I also copied this into the Typescript playground for easier replaying

2 Answers

The absolutely type-safe way of doing it is

interface Something {
    a: string
    b: string
}
function overrideIsSomething(override: unknown): override is Something {
    function isSomethingLike(given: unknown): given is Partial<Record<keyof Something, unknown>> {
        return typeof given === 'object' && given !== null;
    }
    
    return isSomethingLike(override) && typeof override.a === 'string' && typeof override.b === 'string'
}

So you first assert it has a necessary shape then check its properties types.

In your case, a User-Defined type guards would work:

interface Something {
    a: string
    b: string
}

function overrideIsSomething(override: any): override is Something {
    return typeof override.a === 'string' && typeof override.b === 'string'
}

You can use overrideIsSomething like this:

if (overrideIsSomething(override)) {
    // override is Something in this context
    const {a, b} = override;
    return {
        a,
        b,
    };
  }

EDITED

Leverage optional chaining for runtime safe in this specific case:

function overrideIsSomething(override: any): override is Something {
    return typeof override?.a === 'string' && typeof override?.b === 'string'
}

It's simpler and works, but if you want the most type safe way, check zerkms's answer.

Related