在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
类型推导就是在没有明确指出类型的地方,TypeScript编译器会自己去推测出当前变量的类型。 例如下面的例子: let a = 1; 我们并没有明确指明a的类型,所以编译器通过结果反向推断变量a的类型为number,这种推断发生在初始化变量和成员,设置默认参数值和函数有返回值时。 大多数情况下,类型推导是直截了当的,但也有很复杂的情况,例如需要去匹配参数来推测类型。 当需要从几个表达式中推断类型时候,会使用这些表达式的类型来推断出一个最合适的通用类型。例如, let x = [0, 'fanqi', null]; //(string | number)[]
为了推断
由于最终的通用类型取自候选类型,有些时候候选类型共享相同的通用类型,但是却没有一个类型能做为所有候选类型的类型。例如: let zoo = [new Rhino(), new Elephant(), new Snake()]; 这里,我们想让zoo被推断为 let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()]; 如果没有找到最佳通用类型的话,类型推断的结果为联合数组类型, 上下文类型TypeScript类型推论也可能按照相反的方向进行。 这被叫做“按上下文归类”。按上下文归类会发生在表达式的类型与所处的位置相关时。比如: window.onmousedown = function(mouseEvent) { console.log(mouseEvent.button); //<- Error }; 这个例子会得到一个类型错误,TypeScript类型检查器使用 如果上下文类型表达式包含了明确的类型信息,上下文的类型被忽略。 重写上面的例子: window.onmousedown = function(mouseEvent: any) { console.log(mouseEvent.button); //<- Now, no error is given }; 这个函数表达式有明确的参数类型注解,上下文类型被忽略。 这样的话就不报错了,因为这里不会使用到上下文类型。 上下文归类会在很多情况下使用到。 通常包含函数的参数,赋值表达式的右边,类型断言,对象成员和数组字面量和返回值语句。 上下文类型也会做为最佳通用类型的候选类型。比如: function createZoo(): Animal[] { return [new Rhino(), new Elephant(), new Snake()]; } 这个例子里,最佳通用类型有4个候选者: 类型兼容性TypeScript里的类型兼容性是基于结构子类型的。 结构类型是一种只使用其成员来描述类型的方式。 它正好与名义(nominal)类型形成对比。(译者注:在基于名义类型的类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。这与结构性类型系统不同,它是基于类型的组成结构,且不要求明确地声明。) 看下面的例子: interface Person { name: string; } class Father { name: string; } let person: Person; // OK, because of structural typing person = new Father(); 在使用基于名义类型的语言,例如C#或Java中,这段代码会报错,因为Father类并没有明确说明其实现了Person接口。 TypeScript的结构性子类型是根据JavaScript代码的典型写法来设计的。 因为JavaScript里广泛地使用匿名对象,例如函数表达式和对象字面量,所以使用结构类型系统来描述这些类型比使用名义类型系统更好。 关于可靠性的注意事项我们可以看到在以上的类型中,只要满足了子结构的描述,那么它就可以通过编译时检查,所以TypeScript的设计思想并不是满足正确的类型,而是满足能正确通过编译的类型,这就造成了运行时和编译时可能存在类型偏差。 所以TypeScript的类型系统允许某些在编译时无法确认其安全性的操作。当一个类型系统具有此属性时,被认为是“不可靠”的。而TypeScript允许这种不可靠行为的发生是经过仔细考虑的。下面我们会解释为什么需要这种特性。
开始 TypeScript结构化类型系统的基本规则是,如果 interface Person { name: string; } let person: Person; let y = { name: 'fanqi', age: 25}; person = y; 当将y赋值给person时,编译器会检查person中的每个属性,看是否能在y中也找到对应的属性。 在这个例子中,编译器发现y中也含有name属性,那赋值就是正确的,即使事实上并不准确。 检查函数参数时使用相同的规则: function greetTo(person: Person) { console.log('Hello, ' + person.name); } greetTo(y); // OK 这个比较过程是递归进行的,检查每个成员及子成员。 比较两个函数相对来讲,在比较原始类型和对象类型的时候是比较容易理解的,而在判断两个函数返回值是否相等时,TypeScript比对的是函数签名。 一个函数里面包含了参数及返回值,我们可以看一看下面这个例子: let x = (a: number) => 0; let y = (b: number, s: string) => 0; y = x; // OK x = y; // Error 要查看 x=y会引发赋值错误,因为 你可能会疑惑,为什么允许x let items = [1, 2, 3]; // Don't force these extra arguments items.forEach((item, index, array) => console.log(item)); // Should be OK! items.forEach((item) => console.log(item)); 下面来看看如何处理返回值类型,创建两个仅是返回值类型不同的函数: let x = () => ({name: 'Alice'}); let y = () => ({name: 'Alice', location: 'Seattle'}); x = y; // OK y = x; // Error, because x() lacks a location property 类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。 我们再来看一个更复杂的情况,叫ReturnType,这个类型也是写在typescript/lib/lib.es5.d.ts中 /** * Obtain the return type of a function type */ type ReturnType<T extends (...args: any[]) => any> = T extends (...args:any[]) => infer R ? R : any; ReturnType的使用是这样的: let x = (a:number) => ({a,b:'hello'}); type xReturnType = ReturnType<typeof x> // type xReturnType = { // a:number; // b:string; // } infer关键字可以帮助我们引入一个待推断的类型变量,这个待推断的类型变量在推断成立时会写入类型,而在失败时会回退为any。 到目前为止,我们简单的梳理了一遍TypeScript的类型推导,也初步了解了infer的使用。了解类型推导主要是为了使我们能动态的获取类型,减少手动标注类型的工作量,提升效率。 函数参数双向协变当比较函数参数类型时,只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。 这是不稳定的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息。 实际上,这极少会发生错误,并且能够实现很多JavaScript里的常见模式。例如: enum EventType { Mouse, Keyboard } interface Event { timestamp: number; } interface MouseEvent extends Event { x: number; y: number } interface KeyEvent extends Event { keyCode: number } function listenEvent(eventType: EventType, handler: (n: Event) => void) { /* ... */ } // Unsound, but useful and common listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y)); // Undesirable alternatives in presence of soundness listenEvent(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y)); listenEvent(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y))); // Still disallowed (clear error). Type safety enforced for wholly incompatible types listenEvent(EventType.Mouse, (e: number) => console.log(e)); 可选参数及剩余参数比较函数兼容性的时候,可选参数与必须参数是可互换的。 源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型里没有对应的参数也不是错误。 当一个函数有剩余参数时,它被当做无限个可选参数。 这对于类型系统来说是不稳定的,但从运行时的角度来看,可选参数一般来说是不强制的,因为对于大多数函数来说相当于传递了一些 有一个好的例子,常见的函数接收一个回调函数并用对于程序员来说是可预知的参数但对类型系统来说是不确定的参数来调用: function invokeLater(args: any[], callback: (...args: any[]) => void) { /* ... Invoke callback with 'args' ... */ } // Unsound - invokeLater "might" provide any number of arguments invokeLater([1, 2], (x, y) => console.log(x + ', ' + y)); // Confusing (x and y are actually required) and undiscoverable invokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y)); 函数重载对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。 这确保了目标函数可以在所有源函数可调用的地方调用。 枚举枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。比如, enum Status { Ready, Waiting }; enum Color { Red, Blue, Green }; let status = Status.Ready; status = Color.Green; // Error 类类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。 class Animal { feet: number; constructor(name: string, numFeet: number) { } } class Size { feet: number; constructor(numFeet: number) { } } let a: Animal; let s: Size; a = s; // OK s = a; // OK 类的私有成员和受保护成员类的私有成员和受保护成员会影响兼容性。 当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 同样地,这条规则也适用于包含受保护成员实例的类型检查。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。 泛型因为TypeScript是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。比如, interface Empty<T> { } let x: Empty<number>; let y: Empty<string>; x = y; // OK, because y matches structure of x 上面代码里, interface NotEmpty<T> { data: T; } let x: NotEmpty<number>; let y: NotEmpty<string>; x = y; // Error, because x and y are not compatible 在这里,泛型类型在使用时就好比不是一个泛型类型。 对于没指定泛型类型的泛型参数时,会把所有泛型参数当成 比如, let identity = function<T>(x: T): T { // ... } let reverse = function<U>(y: U): U { // ... } identity = reverse; // OK, because (x: any) => any matches (y: any) => any 高级主题子类型与赋值 目前为止,我们使用了“兼容性”,它在语言规范里没有定义。 在TypeScript里,有两种兼容性:子类型和赋值。 它们的不同点在于,赋值扩展了子类型兼容性,增加了一些规则,允许和 语言里的不同地方分别使用了它们之中的机制。 实际上,类型兼容性是由赋值兼容性来控制的,即使在
|
请发表评论