打字稿中可调用类型关系的问题

Posted

技术标签:

【中文标题】打字稿中可调用类型关系的问题【英文标题】:Problem with callable types relationship in typescript 【发布时间】:2020-07-09 02:29:58 【问题描述】:

我对TypeScript中可调用类型之间的关系有以下问题:

type CheckExtends<T, U> = T extends U ? T : never

type NumberFunc = (op: number) => number
type AnyFunc = (op: any) => any;
type UnknownFunc = (op: unknown) => any;

type NumberAny = CheckExtends<NumberFunc, AnyFunc> // NumberFunc
type NumberUnknown = CheckExtends<NumberFunc, UnknownFunc> // never

(On the playground)

通过 cmets 显示 NumberFunc 类型是扩展 AnyFunc 类型而不是 UnknownFunc 类型。

我不明白,因为NumberFunc 类型(number)的参数类型可以同时分配给anyunknown 类型

【问题讨论】:

可能相关:'unknown' vs. 'any' 【参考方案1】:

一旦你引入了any,你就失去了 TypeScript 中的很多类型安全保证。 any 类型故意为unsound 以允许您执行编译器认为不安全的事情。一切都可以分配给 并且 可以从any 分配;它是类型系统的逃生口。因此,NumberFunc 被视为可分配给AnyFunc 也就不足为奇了。您还会看到 AnyFunc 可分配给 NumberFunc

type AnyNumber = CheckExtends<AnyFunc, NumberFunc> // AnyFunc

然后最好忽略any,然后使用类型安全的unknown 转到问题的另一部分:为什么NumberFunc 不能分配给UnknownFunc


为了保持类型安全,函数的参数类型应为contravariant。这意味着函数类型之间的子类型/超类型关系与其参数的关系是相反的。例如,虽然numberunknown子类型,但(x: number) =&gt; void 类型的函数是(x: unknown) =&gt; void超类型。因此,可分配性关系发生了翻转。起初这可能会令人困惑,但从类型安全的角度来看,它实际上是唯一有意义的事情。这是所谓的Liskov Substitution Principle 的结果;如果A 可分配给B,这意味着您可以替换任何B 类型的值与A 类型之一,它不会导致任何错误或问题。

假设我有一个类型为number 的值n。如果您要求unknown 类型的值,如果我给您n,您会很高兴。如果我给你"hello",或false,或任何值,你会很高兴。因为numberunknown 的子类型,所以您可以在任何需要unknown 的地方使用number

但现在假设我有一个 (x: number) =&gt; void 类型的值 f。如果你要一个(x: unknown) =&gt; void 类型的值,而我把f 交给你,你在尝试使用它时可能会很不高兴。您认为您有一个接受任何可能参数的函数,因此您希望能够像f("hello")f(false)f(15) 一样调用它。但是f()只接受number参数,所以f("hello")f(false)很可能在运行时爆炸。您要求的功能非常特定。因为在某些地方不能使用(x: number) =&gt; void,所以可以使用(x: unknown) =&gt; void,那么前者不是后者的子类型。

实际上是相反的:如果我有一个g 类型为(x: unknown) =&gt; void 的值,而你要求一个(x: number) =&gt; void 类型的值,我可以把g 交给你,你会很高兴。您只会使用g(1)g(2)g(15) 等数字参数调用g(),因此您在运行时永远不会遇到问题。这意味着UnknownFuncNumberFunc 的子类型:

type UnknownNumber = CheckExtends<UnknownFunc, NumberFunc> // UnknownFunc

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

Playground link to code

【讨论】:

非常感谢@jcalz。现在对我来说很清楚了。【参考方案2】:

正如您可以see in this article 一样,只有在启用strictFunctionChecks 时目标函数的参数可分配给源函数的参数时,打字稿函数才可分配。例如,如果您有以下代码:

interface A 
    a: number;


interface B extends A 
    b: number;

type AFunc = (op: A) => any;
type BFunc = (op: B) => any;
let bf: BFunc = x => x;
let af: AFunc = bf;

这将出错,因为您无法将BFunc 分配给Afunc,因为目标参数类型A 不能分配给源参数类型B。请注意,检查AFunc extends BFunc 是否等同于检查AFunc 是否可分配给BFunc。您不能将NumberFunc 分配给UnknownFunc,因为目标参数类型unknown 不能分配给源参数类型number。这意味着NumberFunc 不会扩展UnknownFunc。如果你在操场上禁用strictFunctionChecks,你会看到NumberUnknown不再是never,它变成了NumberFunc

【讨论】:

以上是关于打字稿中可调用类型关系的问题的主要内容,如果未能解决你的问题,请参考以下文章

打字稿中数组的两种写作有啥区别

打字稿中的通用对象类型

打字稿中的全局类型

通用类型(T)与打字稿中的任何类型有啥区别

如何在打字稿中声明函数类型

如何在打字稿中声明全局变量