Type guard that asserts all rest parameters are not undefined
Asked Answered
G

1

12

Is it possible to make a user defined type guard that will let the compiler know that all of the arguments passed to it are defined?

I'd like to do something like this:

  public static all(...values: unknown[]): values is object[] {
    return values.every(value => typeof(value) !== 'undefined');
  }

I'd like to use this so that I can pass a set of arguments that are potentially undefined, handle what happens if they are undefined, and otherwise pass them to a method that requires value are not undefined.

That might look like this:

    if (!ParamHelper.all(id, ...dateParts)) { return []; }
    const date = new Date(dateParts.join('-'));
    const result = await this.service.getData(assetId, date);

When I try the naive version above, the compiler tells me:

a type predicate cannot reference a rest parameter

So, I'm currently assuming it can't be done. But, I figured I'd ask before giving up.

Thanks!

Gigigigli answered 31/10, 2019 at 3:7 Comment(7)
You can't guard multiple values at the same time. Not sure that is required for your example however: typescriptlang.org/play/#code/… – Encompass
Type guards only act on usefully on single variables. The rest parameter is either multiple parameters (in which case you could only affect one of them), or it's a single array that you can't access outside of the function implementation (in which case the narrowing is not observable to you). Either way, it doesn't work 😞. I think you'll need to do const a = [id, ...dateParts]; if (!all(a){}{}) where all is of type (x: unknown[])=>x is object[]. (note that null is not considered assignable to object in TypeScript; might be a wrinkle in your implementation.) – Coverall
Both of those are super clever! TypeScript is amazing! They don't quite get me to a point where I can somewhat arbitrarily assert that a set of variables have values. I mean, they do if they're the same type. It's presently easier for me to say if (!id || !day || !month || !year) { return; }. – Gigigigli
@jcalz, you could include your comment as an answer. The point that rest parameters could be one thing shows why it's best to just inline what is being checked for me. – Unwish
TypeScript GitHub issue: github.com/microsoft/TypeScript/issues/26916 – Farrington
@Coverall what about with rest/spread types? – Farrington
I am hoping someone can explain the conclusion of the comments here. Perhaps make it an answer πŸ™ – Communication
F
0

You could pass an array to the function instead of using rest parameters:

public static all(values: unknown[]): values is object[] {
    return values.every(value => typeof(value) !== 'undefined');
}

However the code is technically incorrect since you are asserting that the values are objects when you don't know anything beyond the fact that they are not undefined. You should either check if typeof is equal to 'object' instead or correct the return type:

public static all(values: unknown[]): values is (string|number|object|symbol|boolean)[] {
    return values.every(value => value !== null && typeof value !== 'undefined');
  }
}

Note that typeof null === "object" unfortunately so neither comparisson will weed out null values. I've taken the liberty of adding a null check above.

Now if you use the code as such:

if (!ParamHelper.all([id, ...dateParts])) { return []}
const date = new Date(dateParts.join('-'));

And you go and hover over dateParts in the second line, you will see that the typeguard did not work. Frustratingly dateParts is still undefined. But it's easy to understand why and it might help you understand why it doesn't work with the rest param:

const checked = [id, ...dateParts];
if (!ParamHelper.all(checked)) return [];
const checkedDateParts = checked.slice(1);
const date = new Date(checkedDateParts.join('-'));

Here if you hover over checked or checkedDateParts you will notice that they are of the expected type. In the case above as well as with the rest parameter the array whose type is being determined is thrown away at the end of the function call.

Typescript is not yet smart enough to retroactively pass down typing asserted on an array into the variables that were used to construct it. Given that the array might be constructed via conditional or iterative logic its reasonable to expect it will never be able to do so.

The same is true for the rest parameter, the values array is created implicitly when the function is called and destroyed as the function returns.

In the example above we keep the reference to the array and therefore also the type, we then extract a typed slice from the typed array. We are not going backwards in time to change the type of dateParts. It is still typed as unknown[].

Personally I think using a type-guard here is overengineering. Given that the context is a date, the code you provided in your comment:

if (!id || !day || !month || !year) { return; }

Is much clearer, shorter and easier to maintain.

If you must use the type guard I would go with something like:

if (!id || !ParamHelper.all(dateParts)) { return }
const date = new Date(dateParts.join('-'));

playground link

Favien answered 24/4 at 17:9 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.