typescriptkeyof

Why do we use 'as keyof'?


    const isTrue = <T>(arg: T): { arg: T; is: boolean } => {
  if (Array.isArray(arg) && !arg.length) {
    return { arg, is: false };
  }
  if (isObject(arg) && !Object.keys(arg as keyof T).length) {
    return { arg, is: false };
  }
  return { arg, is: !!arg };
};

In this code, why do we use arg as keyof T? We checked it is an object with the statement 'isObject(arg)' then it should be an object. But as keyof T means that arg's type will be the keys of the object. So how can arg still act like an object despite we used 'as keyof T'?

I added isObject function here:

const isObject = <T>(par: T): boolean => {
  return typeof par === "object" && !Array.isArray(par) && par !== null;
};

Solution

  • I don't know which tutorial that code came from, but you are right to be confused; given arg of type T, it is almost certainly meaningless to later assert that arg is of type keyof T, especially if you've already checked that arg is an object and not a string or symbol. It's a fun exercise to try to devise situations in which T & keyof T is non-empty, like the literal type "length". But this is not the case here, and even if it were, it's a bizarre thing to do.

    Again, the assertion arg as keyof T isn't true.


    If you don't assert then the compiler complains because arg isn't known to be something that Object.keys() accepts. If you look at the most permissive call signature for Object.keys(), it is

    interface ObjectConstructor {
      keys(o: {}): string[];
    }
    

    The parameter is {}, the so-called "empty object type", and while it usually refers to objects, it also is applicable to even non-object values that can be indexed into (e.g., string is allowed because you can index into them with keys like "toUpperCase" and "length"). In other words, the only values that are not assignable to {} are null and undefined. (See How to undestand relations between types any, unknown, {} and between them and other types?)

    Since the isObject() function you presented does not narrow its input (it returns boolean as opposed to a type predicate; another indication that the tutorial might be problematic), then the compiler has no idea that arg will be an object. And thus if you call Object.keys(arg) directly, the compiler will complain because arg is of type T which might allow null or undefined:

    Object.keys(arg); // error!
    // -------> ~~~
    // Argument of type 'T' is not assignable to parameter of type '{}'.
    

    The mistaken assertion arg as keyof T suppresses that error because keyof T is some subtype of string | number | symbol, each of which is non-nullish.

    The only thing as keyof T does for you is that it narrows arg to something known to be non-nullish. So it suppresses the error, but in a very strange way. It would work just as well to say Object.keys(arg as Date) or Object.keys(arg as Math) or Object.keys(arg as "whoopsieDoodle"). And then you'd be asking similar questions about why Math is relevant. It isn't.


    A reasonable thing to do here would be to assert that arg is the object type, which is presumably what isObject() is meant to do:

    Object.keys(arg as object) // okay
    

    Even more reasonable would be if isObject() were a type guarding function as presumably is intended:

    const isObject = (par: any): par is object => {
      return typeof par === "object" && par !== null;
    };
    
    const isTrue = <T,>(arg: T): { arg: T; is: boolean } => {
      if (Array.isArray(arg) && !arg.length) {
        return { arg, is: false };
      }
      if (isObject(arg) && !Object.keys(arg).length) { // okay
        return { arg, is: false };
      }
      return { arg, is: !!arg };
    };
    

    But this is essentially a digression from the question as asked.


    To reiterate:

    Q: Why do we use arg as keyof T?

    A: Most likely because we are confused and used the first thing we found that suppressed a compiler error. We should really have used arg as object or some other approach entirely. We should probably edit our tutorial, now that we think about it.

    Playground link to code