ReactHelp
ReactHelp

Reputation: 479

Typescript how to use `unknown` type on a parent function to then determine the type on a child function?

I want to be able to determine which interface myValue corresponds to. Given the following, how can I log either a or b depending on the return value of testZero()?

export interface ITest {
  id: string
  isLatest: boolean
  createdAt: number
  updatedAt: number
}

export interface ITestTwo {
  id: string
  isLatest: boolean
  createdAt: string  // This value was changed for this example
  updatedAt: number
}

function testZero(): unknown {
    return {
        id: '123',
        isLatest: true,
        createdAt: '123',
        updatedAt: 456
    }
}

function testOne(): ITest | ITestTwo {
    const myValue = testZero()

    // THE FOLLOWING `IF` STATEMENT DOES NOT WORK AND IS WHAT I AM TRYING TO SOLVE
    if (typeof t === typeof ITest) {
        console.log('a')
    } else if (typeof myValue === typeof ITestTwo) {
        console.log('b')
    }
}

testOne()

Upvotes: 0

Views: 521

Answers (1)

jcalz
jcalz

Reputation: 329543

You will need to write runtime code to check if a value conforms to an interface. The TypeScript interface itself is erased when the TypeScript code is compiled to JavaScript. The TypeScript type system is meant to describe types at runtime; it does not affect runtime. So ITest and ITestTwo will not be around to check against.

Furthermore, the runtime typeof operator will check the type of its operand and return one of a small set of string values; in your case, you can expect typeof myValue to be "object" no matter what. So you can't use typeof to check myValue itself. At best you will need to check myValue's properties and use typeof on them.


One way to do this is to write some type guard functions to represent this runtime checking in a way that the compiler recognizes as a type check. For "simple" object types where you can just check each property using the runtime typeof operator, you can write a generalized checking function and then specialize it for your needs. The generalized function:

type TypeofMapping = {
  string: string;
  boolean: boolean;
  number: number;
  object: object;
  undefined: undefined;
}
const simpleObjGuard = <T extends { [K in keyof T]: keyof TypeofMapping }>
  (obj: T) => (x: any): x is { [K in keyof T]: TypeofMapping[T[K]] } => typeof x === "object" && x &&
    (Object.keys(obj) as Array<keyof T>).every(k => k in x && typeof x[k] === obj[k]);

And then you can specialize it for ITest and ITestTwo:

const isITest: (x: any) => x is ITest =
  simpleObjGuard({ id: "string", isLatest: "boolean", createdAt: "number", updatedAt: "number" });

const isITestTwo: (x: any) => x is ITestTwo =
  simpleObjGuard({ id: "string", isLatest: "boolean", createdAt: "string", updatedAt: "number" });

So the isITest() function will check is its argument is compatible with ITest, and the isITestTwo() function will check if its argument is compatible with ITestTwo. In either case the compiler will interpret a true result as evidence that the argument can be narrowed from, say, unknown, to the type you're checking:

function testOne(): ITest | ITestTwo {
  const myValue = testZero()
  if (isITest(myValue)) {
    console.log("a "+myValue.createdAt.toFixed())
    return myValue;
  } else if (isITestTwo(myValue)) {
    console.log("b "+myValue.createdAt.toUpperCase())
    return myValue;
  }
  throw new Error("Guess it wasn't one of those");
}

The fact that the console.log() and return lines don't cause a compiler error shows that the compiler sees myValue as the narrowed type. And at runtime you can check that it works too:

testOne() // b 123

There are libraries out there which will write these type guard functions for you... I think io-ts is one that does this as well as make serialization/deserialization code in a way that works nicely with the TypeScript compiler. Or you can write your own, like simpleObjGuard above.

Okay, hope that helps; good luck!

Playground link to code

Upvotes: 2

Related Questions