TypeScript:条件类型意外错误

时间:2020-01-24 22:02:02

标签: typescript

我正在尝试一个非常基本的(人为)条件类型函数,并遇到意外错误:

function test<
  T
>(
  maybeNumber: T
): T extends number ? number : string {
  if (typeof maybeNumber === 'number') {
    return maybeNumber // Type 'T & number' is not assignable to type 'T extends number ? number : string'.
  }

  return 'Not a number' // Type '"Not a number"' is not assignable to type 'T extends number ? number : string'.
}

我认为这是条件类型的非常简单的用法,所以不确定发生了什么。有什么想法吗?

为澄清起见,我并不是真的在尝试实现此特定功能。我只是在尝试条件类型,并且想更好地理解为什么它实际上不起作用。

5 个答案:

答案 0 :(得分:5)

潜在的问题是TypeScript的编译器无法通过控制流分析来缩小通用类型变量的类型。当您选中(typeof maybeNumber === "number")时,编译器可以将 value maybeNumber的范围缩小到number,但不会缩小 type参数 { {1}}至T。因此,它无法验证为返回类型number分配number值是安全的。编译器将不得不执行当前不执行的某些分析,例如“好吧,如果T extends number ? number : string,并且我们仅从typeof maybeNumber === "number"的类型推断出T,则在此块中,我们可以将maybeNumber缩小为T,因此我们应该返回number类型的值,也就是number extends number ? number : string“。但这不会发生。

对于带条件返回类型的泛型函数,这是一个很大的痛点。有关此问题的规范的GitHub公开问题可能是microsoft/TypeScript#33912,但是还有其他很多GitHub问题,这是主要问题。

这就是“为什么不起作用”的答案?


如果您对重构使其不起作用不感兴趣,则可以忽略其余部分,但是了解这种情况下的处理方式而不是等待语言更改可能仍然很有帮助。

在这里,维护您的呼叫签名的最直接的解决方法是使函数成为单个签名overload,其中实现签名不是通用的。这实质上放松了实现内部的类型安全保证:

number

在这里,我尽了最大努力来确保类型安全,方法是使用一个临时type MyConditional<T> = T extends number ? number : string; type Unknown = string | number | boolean | {} | null | undefined; function test<T>(maybeNumber: T): MyConditional<T>; function test(maybeNumber: Unknown): MyConditional<Unknown> { if (typeof maybeNumber === 'number') { const ret: MyConditional<typeof maybeNumber> = maybeNumber; return ret; } const ret: MyConditional<typeof maybeNumber> = "Not a number"; return ret; } 变量,其注释为ret,该变量使用控制流分析窄类型MyConditional<typeof maybeNumber>中的。如果您转过支票(将maybeNumber变成===进行验证),这至少会引起抱怨。但是通常我会做这样简单的事情,然后让芯片落在可能的位置:

!==

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

Playground link to code

答案 1 :(得分:1)

正确答案和当前解决方法(在撰写本文时):

type MaybeNumberType<T extends number | string> = T extends number
  ? number
  : string;

function test<T extends number | string>(
  maybeNumber: T,
): MaybeNumberType<T> {
  if (typeof maybeNumber === 'number') {
    return <MaybeNumberType<T>>(<unknown>maybeNumber);
  }

  return <MaybeNumberType<T>>(<unknown>'Not a number');
}

test(3); // 3
test('s'); // Not a number

答案 2 :(得分:0)

我相信您在错误的位置定义了返回类型:

function test<T extends number | String>(maybeNumber: T) {
if (typeof maybeNumber === 'number') {
  return maybeNumber
}
return 'Not a number'

}

答案 3 :(得分:0)

如果使用重载实现,您的功能会更好:

function test(arg: number): number;
function test(arg: unknown): string;
function test(arg: any): string | number {
  if (typeof arg === 'number') {
    return arg;
  }

  return 'Not a number'
}

答案 4 :(得分:0)

问题在于,即使maybeNumber不扩展Tnumber在运行时也可能是数字。在这种情况下,您的函数返回一个number,但是其类型声明表明它应该返回string。考虑:

function test_the_test(value: Object) {
    // no type error here, even though test(value) will return a number at runtime
    let s: string = test(value);
}

test_the_test(23);

在这种情况下,T = Object不会扩展number-它是超类型,而不是子类型-因此条件类型T extends number ? number : string解析为string。我们可以使用通用类型对此进行测试:

type Test<T> = T extends number ? number : string
type TestObject = Test<Object>
// TestObject is string

因此,您的函数test实际上至少在一种声称将返回字符串的情况下返回一个数字。这意味着该函数不是类型安全的,并且正确输入类型错误是正确的。

实际上,给定这些类型注释,没有明智的,类型安全的函数实现。 test<number>(23)应该返回number,但是test<Object>(23)应该返回string;但是两者都编译为相同的Javascript,因此无法在运行时知道函数将返回哪种类型。满足这两个约束的唯一方法是编写一个永不返回的函数(例如通过无条件抛出异常);如果您从不返回值,则返回值永远不会有错误的类型。因此,您的功能需要重新设计。


在Typescript中,有两种明智的编写方法来检查输入是否为数字。一种方法是编写一个user-defined type guard,如果它返回number,则将其参数类型缩小为true

function isNumber(x: any): x is number {
    return typeof x === 'number';
}

另一种方法是编写一个assertion function,如果它不是数字,则抛出一个错误,从而将其参数的类型缩小为number

function assertNumber(x: any): asserts x is number {
    if(typeof x !== 'number') {
        throw new TypeError('Not a number');
    }
}

不幸的是,当输入不是数字时,它们都不返回字符串。但两者都可能适合您的实际用例。