打字稿如何在父函数上使用“未知”类型来确定子函数的类型?

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

我希望能够确定 myValue 对应于哪个接口。鉴于以下情况,我如何根据 testZero() 的 return 值记录 ab

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()

您将需要编写运行时代码来检查值是否符合接口。当 TypeScript 代码编译为 JavaScript 时,TypeScript 接口本身是 erased。 TypeScript 类型系统旨在在运行时描述 类型;它不会影响 运行时。所以 ITestITestTwo 将不会被检查。

此外,runtime typeof operator 将检查其操作数的类型和 return 一小组字符串值中的一个;在你的情况下,你可以期望 typeof myValue 无论如何都是 "object"。所以你不能使用 typeof 来检查 myValue 本身。充其量您需要检查 myValue 的属性并对其使用 typeof


一种方法是编写一些 type guard functions 以编译器识别为类型检查的方式表示此运行时检查。对于您可以使用运行时 typeof 运算符检查每个 属性 的“简单”对象类型,您可以编写一个通用检查函数,然后根据您的需要对其进行专门化。广义函数:

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]);

然后你可以专门为 ITestITestTwo:

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" });

因此 isITest() 函数将检查其参数是否与 ITest 兼容,而 isITestTwo() 函数将检查其参数是否与 ITestTwo 兼容。在任何一种情况下,编译器都会将 true 结果解释为参数可以从 unknown 缩小到您正在检查的类型的证据:

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");
}

console.log()return 行不会导致编译器错误这一事实表明编译器将 myValue 视为窄化类型。在运行时,您可以检查它是否也有效:

testOne() // b 123

有一些库可以为您编写这些类型保护函数...我认为 io-ts 可以做到这一点,并且可以以一种很好的方式编写 serialization/deserialization 代码使用 TypeScript 编译器。或者您可以自己编写,如上面的 simpleObjGuard

好的,希望对您有所帮助;祝你好运!

Playground link to code