我正在try 创建一个可以处理两个互斥返回类型的函数.我对打字的工作原理有什么误解?

type SuccessResponse<T> = { data: T; error: null };
type ErrorResponse<E> = { data: null; error: NonNullable<E> };
type Command<T, E> = () => ErrorResponse<E> | SuccessResponse<T>;

const myFunc = <T, E>(c: Command<T, E>): T => { 
  const result = c();
  if (result.error !== null) throw result.error;
  return result.data;
}

我在返回时收到以下错误:

Type 'T | null' is not assignable to type 'T'.
  'T' could be instantiated with an arbitrary type which could be unrelated to 'T | null'.(2322)

我不能只测试if (result.data),因为Tnull是可以接受的.


下面是一个更长的示例,它解释了想要错误类型的动机:这样实用程序就可以断言并存储它以供以后使用.(假设makeRefreshablemyCommand都不在我的控制之下)

type SuccessResponse<T> = { data: T; error: null };
type ErrorResponse<E> = { data: null; error: NonNullable<E> };
type Command<T, E> = () => Promise<ErrorResponse<E> | SuccessResponse<T>>;

const myCommand = async () => {
  if (Math.random() < .1) return {data: null, error: new Error('Random error')};
  return await { data: Math.random(), error: null};
}

const getCommandResult = async <T, E>(c: Command<T, E>): Promise<T> => {
  const result = await c();
  if (result.error !== null) throw result.error;
  return result.data;
}

const makeRefreshable = <T, E>(f: () => Promise<T>) => {
  const result: {
    data: T | null;
    error: E | null;
    run: () => void;
  } = {
    data: null,
    error: null,
    run: () => {},
  }

  result.run = async () => {
    try {
      result.data = await f();
    } catch (err) {
      result.data = null;
      result.error = err as E;
    }
  }

  return result;
}

const main = async () => {
  const result = makeRefreshable<number, Error>(() => getCommandResult(myCommand));
  await result.run();
  if (result.data) console.log(result.data);
  if (result.error) console.log(result.error.message);
  await result.run();
  if (result.data) console.log(result.data);
  if (result.error) console.log(result.error.message);
}

main();

推荐答案

TypeScrip不支持throws子句(如在Java中),因此类型系统无法表达函数抛出指定类型的错误的 idea .也就是说,你不能说declare function myFunc(): void throws MyError这样的话.

也没有一种方法可以只对特定预期类型的异常进行catch处理,因为catch in JavaScript将捕获抛出的任何异常,并且可能发生意外错误. 如果你写try { myFunc() } catch (err: MyError) { },很有可能err在运行时完全不同于MyError. 在这种情况下,catch条款不会被跳过. 因此,即使你可以用throws子句注释函数,它们在catch时也不会有很大帮助.

microsoft/TypeScript#13219上有一个长期的功能请求,支持throwscatch条款,但最终被拒绝了. 如果你感兴趣,有一个very thorough comment详细说明了为什么它被拒绝. 其结果是,判断异常并不真正适合JavaScript语言和生态系统,将其添加到TypeScript将引入更多的问题,而不是它声称要解决的问题.


因此,这意味着让getCommandResult()在抛出的错误类型E中为generic真的没有意义.getCommandResult()的返回类型是Promise<T>,没有提到E,并且您期望捕获类型E的错误的带外信息不是编译器可以保证或跟踪的.

因此,你不妨把E换成unknown,这对你有好处:

const getCommandResult = async <T,>(c: Command<T, unknown>): Promise<T> => {
  const result = await c();
  if (result.error !== null) throw result.error;
  return result.data;
}

现在,在实现中没有任何错误.

E是泛型时的问题是编译器不能对泛型类型进行任意分析;虽然对我们来说显然空的error属性应该将ErrorResponse<E> | SuccessResponse<T>缩小到SuccessResponse<T>,但这需要编译器判断ErrorResponse<E>的 struct ...但当E是泛型时,编译器不幸地在它走到这一步之前就放弃了,而当涉及到缩小范围时,ErrorResponse<E>基本上是不透明的.在microsoft/TypeScript#55036岁的时候,有一个悬而未决的问题要求改进,但目前还没有实施.如果你说的是通用的needed E,我们就不得不绕过它.

但是,既然让E成为通用的没有意义,我们不妨只使用unknown,并从control flow analysis中受益.


无论如何,当您将其称为getCommandResult()时,出于上述原因,您将需要判断catch块中的err是否为您所期望的类型.不幸的是,这是无可奈何的:

class MyError extends Error {
  timestamp: Date = new Date();
  constructor(msg: string) {
    super(msg)
  }
}

const myCommand: Command<number, MyError> = async () => {
  if (Math.random() < .1) return { data: null, error: new MyError('Random error') };
  return await { data: Math.random(), error: null };
}

async function foo() {
  try {
    const result = await getCommandResult(myCommand);
    console.log("SUCCESS: " + result)
  } catch (err) {
    if (err instanceof MyError) {
      console.log("ERROR AT: " + err.timestamp.toString())
    } else {
      console.log("UNEXPECTED ERROR: " + err) // 🤷‍♂️
    }
  }
}
foo(); // "SUCCESS: 0.9875369808095005" 
foo(); // "ERROR AT: Tue Dec 05 2023 20:49:51 GMT-0600 (Central Standard Time)" 
foo(); // "SUCCESS: 0.11642966130918875" 

这差不多是它得到的最好的结果了.对MyError的判断必须在运行时进行.

Playground link to code

Typescript相关问答推荐

等待的R没有用响应对象展开到R

如何为ViewContainerRef加载的Angular组件实现CanDeactivate保护?

node—redis:如何在redis timeSeries上查询argmin?

类型脚本`Return;`vs`Return unfined;`

我可以为情态车使用棱角形的路由守卫吗?

被推断为原始类型的交集的扩充类型的交集

有什么方法可以类型安全地包装import()函数吗?

使用泛型识别对象值类型

缩小对象具有某一类型的任意字段的范围

将布尔值附加到每个对象特性

三重融合视角与正交阵

如何在Reaction Native中关注屏幕时正确更新状态变量?

如何在Type脚本中创建一个只接受两个对象之间具有相同值类型的键的接口?

有没有什么方法可以使用ProvideState()提供多个削减器作为根减减器

在Vite中将SVG导入为ReactComponent-不明确的间接导出:ReactComponent

T的typeof键的Typescript定义

Typescript循环函数引用

在Typescript 无法正常工作的 Three.js 中更新动画中的 GLSL 统一变量

根据索引将元组拆分为两个块

为什么 typescript 在对象合并期间无法判断无效键?