知识导向
- 类型推论
- 基础
- 多类型联合
- 上下文类型
- 类型兼容性
- 基础
- 函数兼容性<参数个数、参数类型、返回值类型、可选参数和剩余参数、参数双向协变、函数重载>
- 枚举
- 类
- 泛型
一、类型推论
1.1 基础
TypeScript里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。如下面的例子
let str = 'string'
变量str
被推断为字符串类型。这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时。
大多数情况下,类型推论是直截了当地。 后面的小节,我们会浏览类型推论时的细微差别。
1.2 多类型联合
let x = [0, 1, null]
为了推断x
的类型,我们必须考虑所有元素的类型。 这里有两种选择:number
和null
。 计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。
由于最终的通用类型取自候选类型,有些时候候选类型共享相同的通用类型,但是却没有一个类型能做为所有候选类型的类型。例如:
let zoo = [new Rhino(), new Elephant(), new Snake()];
这里,我们想让zoo被推断为Animal[]
类型,但是这个数组里没有对象是Animal
类型的,因此不能推断出这个结果。 为了更正,当候选类型不能使用的时候我们需要明确的指出类型:
let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];
如果没有找到最佳通用类型的话,类型推断的结果为联合数组类型,(Rhino | Elephant | Snake)[]
。
1.3 上下文类型
TypeScript类型推论也可能按照相反的方向进行。 这被叫做“按上下文归类”。按上下文归类会发生在表达式的类型与所处的位置相关时。比如:
window.onmousedown = (mouseEvent) => {
console.log(mouseEvent);
}
这个例子会得到一个类型错误,TypeScript类型检查器使用Window.onmousedown
函数的类型来推断右边函数表达式的类型。 因此,就能推断出mouseEvent
参数的类型了。 如果函数表达式不是在上下文类型的位置,mouseEvent
参数的类型需要指定为any
,这样也不会报错了。
如果上下文类型表达式包含了明确的类型信息,上下文的类型被忽略。 重写上面的例子:
window.onmousedown = function(mouseEvent: any) {
console.log(mouseEvent.button);
};
这个函数表达式有明确的参数类型注解,上下文类型被忽略。 这样的话就不报错了,因为这里不会使用到上下文类型。
上下文归类会在很多情况下使用到。 通常包含函数的参数,赋值表达式的右边,类型断言,对象成员和数组字面量和返回值语句。 上下文类型也会做为最佳通用类型的候选类型。比如:
function createZoo(): Animal[] {
return [new Rhino(), new Elephant(), new Snake()];
}
这个例子里,最佳通用类型有4个候选者:Animal
,Rhino
,Elephant
和Snake
。 当然,Animal
会被做为最佳通用类型。
二、类型兼容性
2.1 基础
TypeScript结构化类型系统的基本规则是,如果x
要兼容y
,那么y
至少具有与x
相同的属性。比如:
interface Info {
name: string
}
let infos: Info
const infos1 = { name: 'cyang' }
const infos2 = { age: 18 }
const infos3 = { name: 'cyang', age: 18 }
infos = infos1
infos = infos2 // error 报错不符合接口类型
infos = infos3
同样,类型兼容性也支持递归嵌套检测。
interface Info {
name: string,
info: { age: number }
}
let infos: Info
const infos4 = { name: 'cyang', info: { age: 18 } }
2.2 函数兼容性
2.2.1 参数个数
let x = (a: number) => 0
let y = (b: number, c: string) => 0
y = x
// x = y //error 不能将类型“(b: number, c: string) => number”分配给类型“(a: number) => number”。
要查看x
是否能赋值给y
,首先看它们的参数列表。x
的每个参数必须能在y
里找到对应类型的参数。 注意的是参数的名字相同与否无所谓,只看它们的类型。 这里,x
的每个参数在y
中都能找到对应的参数,所以允许赋值。
第二个赋值错误,因为y
有个必需的第二个参数,但是x
并没有,所以不允许赋值。
下面有一个常见的案例:
let arr = [1, 2, 3]
arr.forEach((item, index, array) => {
console.log(item);
})
arr.forEach((item) => {
console.log(item);
})
Array#forEach
给回调函数传3个参数:数组元素,索引和整个数组。 尽管如此,传入一个只使用第一个参数的回调函数也是很有用的。
2.2.2 参数类型
let x = (a: number) => 0
let y = (b: string) => 0
x = y //error 参数“a”和“b” 的类型不兼容。
y = x //error 参数“a”和“b” 的类型不兼容。
参数的类型不一致同样也是不兼容的。
2.2.3 返回值类型
let x = () => ({name: 'cyang'});
let y = () => ({name: 'cyang', sex: '男'});
x = y; // OK
y = x; // Error,不能将类型“() => { name: string; }”分配给类型“() => { name: string; sex: string; }”。
类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。
2.2.4 可选参数和剩余参数
比较函数兼容性的时候,可选参数与必须参数是可互换的。 源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型里没有对应的参数也不是错误。
const getSum = (arr: number[], callback: (...args: number[]) => number): number => {
return callback(...arr);
}
const res = getSum([1, 2, 3], (...args: number[]): number => args.reduce((a, b) => a + b, 0))
console.log(res);
同时可以这样写。
const res2 = getSum([1, 2, 3], (arg1: number, arg2: number): number => arg1 + arg2)
console.log(res2);
2.2.5 函数参数双向协变
当比较函数参数类型时,只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。 这是不稳定的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息。
let funcA = (arg: number | string): void => { }
let funcB = (arg: number): void => { }
funcA = funcB
funcB = funcA
可以手动配置不使用这个双向协变,后面会讲到。
2.2.6 函数重载
对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。 这确保了目标函数可以在所有源函数可调用的地方调用。
function merge(arg1: number, arg2: number): number
function merge(arg1: string, arg2: string): string
function merge(arg1: any, arg2: any) {
return arg1 + arg2
}
//这里定义了merge函数的两个重载
function sum(arg1: number, arg2: number): number
function sum(arg1: any, arg2: any) {
return arg1 + arg2
}
let func = merge
func = sum //Error 不能将类型“(arg1: number, arg2: number) => number”分配给类型“{ (arg1: number, arg2: number): number; (arg1: string, arg2: string): string; }”。
若想成立,只用将merge函数带有string参数类型的函数签名去掉即可。
2.3 枚举
枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。比如,
enum StatusEnum {
On,
Off,
}
enum AnimalEnum {
Dog,
Cat,
}
let s = StatusEnum.On
s = AnimalEnum.Dog //Error 不能将类型“AnimalEnum.Dog”分配给类型“StatusEnum”。
2.4 类
类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。
class AnimalClass {
public static age: string
constructor(public name: string) { }
}
class PeopleClass {
public static age: string
constructor(public name: string) { }
}
class FoodClass {
constructor(public name: number) { }
}
let animal: AnimalClass
let people: PeopleClass
let food: FoodClass
animal = people
people = animal
animal = food //Error不能将类型“FoodClass”分配给类型“AnimalClass”。属性“name”的类型不兼容。
类的私有成员和受保护成员会影响兼容性。 当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 同样地,这条规则也适用于包含受保护成员实例的类型检查。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。
class ParentClass {
private age: number
constructor() { }
}
class ChildClass extends ParentClass {
constructor() {
super()
}
}
class OtherClass {
private age: number
constructor() { }
}
const children: ParentClass = new ChildClass()
const other: ParentClass = new OtherClass() //Error不能将类型“OtherClass”分配给类型“ParentClass”。类型具有私有属性“age”的单独声明。
2.5 泛型
因为TypeScript是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。比如,
interface Data<T> { }
let data1: Data<number>
let data2: Data<string>
data1 = data2 //OK
上面代码里,data1
和data2
是兼容的,因为它们的结构使用类型参数时并没有什么不同。 把这个例子改变一下,增加一个成员,就能看出是如何工作的了:
interface Data<T> { data: T }
let data1: Data<number>
let data2: Data<string>
data1 = data2 //Error不能将类型“Data<string>”分配给类型“Data<number>”。
在这里,泛型类型在使用时就好比不是一个泛型类型。
对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any
比较。 然后用结果类型进行比较,就像上面第一个例子。
比如,
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