TS
函数类型
这里的 ?: 表示参数可以缺省、可以不传,也就是说调用函数时,我们可以不显式传入参数,它实际上是xxx | undefined。但是,如果我们声明了参数类型为 xxx | undefined 会有不一样的结果
function log(x?: string) {
console.log(x);
}
function log1(x: string | undefined) {
console.log(x);
}
function log2(x: string = '哈哈') {
console.log(x);
}
log();
log(undefined);
log1(); // ts(2554) Expected 1 arguments, but got 0
log1(undefined);
log2()
log2(undefined)
log2(3) //类型“number”的参数不能赋给类型“string”的参数。
function sum(...nums: number[]) {
return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2); // => 3
sum(1, 2, 3); // => 6
sum(1, '2'); // ts(2345) Argument of type 'string' is not assignable to parameter of type 'number'
函数中的this
使用了 TypeScript 后,通过指定 this 的类型(严格模式下,必须显式指定 this 的类型),当我们错误使用了 this,TypeScript 就会提示我们,如下代码所示:
function say() {
console.log(this.name); // ts(2683) 'this' implicitly has type 'any' because it does not have a type annotation
//在上述代码中,如果我们直接调用 say 函数,this 应该指向全局 window 或 global(Node 中)。但是,在 strict 模式下的 TypeScript 中,它会提示 this 的类型是 any,此时就需要我们手动显式指定类型了。
}
say();
在 TypeScript 中,我们只需要在函数的第一个参数中声明 this 指代的对象(即函数被调用的方式)即可
注意:显式注解函数中的 this 类型,它表面上占据了第一个形参的位置,但并不意味着函数真的多了一个参数,因为 TypeScript 转译为 JavaScript 后,“伪形参” this 会被抹掉,这算是 TypeScript 为数不多的特有语法。
function say(this: Window, name: string) {
console.log(this.name);
}
window.say = say;
window.say('hi');
const obj = {
say
};
obj.say('hi'); // ts(2684) The 'this' context of type '{ say: (this: Window, name: string) => void; }' is not assignable to method's 'this' of type 'Window'.
interface Person {
name: string;
say(this: Person): void;
}
const person: Person = {
name: 'captain',
say() {
console.log(this.name);
},
};
const fn = person.say;
person.say()
fn(); // ts(2684) The 'this' context of type 'void' is not assignable to method's 'this' of type 'Person'
class Component {
onClick(this: Component) {}
}
const component = new Component();
interface UI {
addClickListener(onClick: (this: void) => void): void;
}
const ui: UI = {
addClickListener() {}
};
ui.addClickListener(new Component().onClick); // ts(2345)
函数重载
JavaScript 是一门动态语言,针对同一个函数,它可以有多种不同类型的参数与返回值,这就是函数的多态。而在 TypeScript 中,也可以相应地表达不同类型的参数和返回值的函数
注意:有实现体的函数(第四行),是不能直接被调用的;函数重载列表的各个成员(即示例中的 1 ~ 3 行)必须是函数实现(即示例中的第 4 行)的子集,例如 “function convert(x: string): number”是“function convert(x: string | number | null): any”的子集。
在 convert 函数被调用时,TypeScript 会从上到下查找函数重载列表中与入参类型匹配的类型,并优先使用第一个匹配的重载定义。因此,我们需要把最精确的函数重载放到前面。
function convert(x: string): number;
function convert(x: number): string;
function convert(x: null): -1;
function convert(x: string | number | null): any {
if (typeof x === 'string') {
return Number(x);
}
if (typeof x === 'number') {
return String(x);
}
return -1;
}
const x1 = convert('1'); // => number
const x2 = convert(1); // => string
const x3 = convert(null); // -1
interface P1 {
name: string;
}
interface P2 extends P1 {
age: number;
}
function convert(x: P1): number;
function convert(x: P2): string;
function convert(x: P1 | P2): any {}
const x1 = convert({ name: "" } as P1); // => number
const x2 = convert({ name: "", age: 18 } as P2); // number
//因为 P2 继承自 P1,所以类型为 P2 的参数会和类型为 P1 的参数一样匹配到第一个函数重载,此时 x1、x2 的返回值都是 number。
//而我们只需要将函数重载列表的顺序调换一下
interface接口类型
定义内联的接口类型是不可复用的,所以我们应该更多地使用interface关键字来抽离可复用的接口类型
interface ProgramLanguage {
name: string;
age: () => number;
author:string
}
interface ProgramLanguage1 {
readonly name: string;
age: () => number;
author?:string
}
function NewStudy(language: ProgramLanguage) {
console.log(`ProgramLanguage ${language.name} created ${language.age()} years ago.`);
}
let TypeScript:ProgramLanguage1= {
name: 'TypeScript',
age: () => new Date().getFullYear() - 2012,
}
NewStudy(TypeScript) //ts2345 属性“author”的类型不兼容。 不能将类型“string | undefined”分配给类型“string”。
索引签名
索引名称的类型分为 string 和 number 两种,通过如下定义的 LanguageRankInterface 和 LanguageYearInterface 两个接口,我们可以用来描述索引是任意数字或任意字符串的对象。
interface LanguageRankInterface {
readonly [rank: number]: string;
}
interface LanguageYearInterface {
[name: string]: number;
}
{
let LanguageRankMap: LanguageRankInterface = {
1: 'TypeScript', // ok
2: 'JavaScript', // ok
'WrongINdex': '2012' // ts(2322) 不存在的属性名
};
let LanguageMap: LanguageYearInterface = {
TypeScript: 2012, // ok
JavaScript: 1995, // ok
1: 1970 // ok
};
//注意:在上述示例中,数字作为对象索引时,它的类型既可以与数字兼容,也可以与字符串兼容,这与 JavaScript 的行为一致。因此,使用 0 或 '0' 索引对象时,这两者等价
//利用联合类型实现除了age是number,其他key是string
type a = {
age:never,
[key:string]:string
}
type b = {
age:number
}
const c : a | b = {
age:1,
name:'111'
}
}
注意:虽然属性可以与索引签名进行混用,但是属性的类型必须是对应的数字索引或字符串索引的类型的子集,否则会出现错误提示。
interface StringMap {
[prop: string]: number;
age: number; // ok
name: string; // ts(2411) name 属性的 string 类型不能赋值给字符串索引类型 number
}
interface NumberMap {
[rank: number]: string;
1: string; // ok
0: number; // ts(2412) 0 属性的 number 类型不能赋值给数字索引类型 string
}
接口类型继承
{
interface DynamicLanguage {
rank: number; // 定义新属性
}
interface TypeSafeLanguage{
typeChecker: string; // 定义新的属性
}
/** 继承多个 */
interface TypeScriptLanguage extends DynamicLanguage, TypeSafeLanguage {
name: 'TypeScript'; // 用原属性类型的兼容的类型(比如子集)重新定义属性
}
}
类实现接口
interface ProgramLanguage {
name:string,
age:()=>number
}
{
class LanguageClass implements ProgramLanguage {
name: string = '';
age = () => new Date().getFullYear() - 2012
}
}
Type 类型别名
类型别名,诚如其名,即我们仅仅是给类型取了一个新的名字,并不是创建了一个新的类型。
{
// &的优先级要高级|
/** 联合 */
type MixedType = string | number;
/** 交叉 */
type IntersectionType = { id: number; name: string; } & { age: number; name: string };
/** 提取某个接口属性的类型 */
type AgeType = ProgramLanguage['age'];
// 因为联合类型会被缩减,这会大大降低IDE的提示能力,但是TS中有个黑魔法
type color = 'black' | 'red' | string & {}
}
Interface 与 Type 的区别
-
重复定义的接口类型,它的属性会叠加,这个特性使得我们可以极其方便地对全局变量、第三方库的类型做扩展.如果我们重复定义类型别名,如下代码所示,则会提示一个 ts(2300) 错误。
-
interface 只能定义对象/函数类型, 而 type 声明可以声明任何类型,包括基础类型、联合类型或交叉类型元组类型,如
type yuanzu = [myType1, myType2]。 -
interface可以extends、implements,从而扩展多个接口或类。类型没有扩展功能,只能交叉合并。
-
type 可以获取 typeof 返回的值作为类型
{
interface Language {
id: number;
}
interface Language {
name: string;
}
let lang: Language = {
id: 1, // ok
name: 'name' // ok
}
}
{
/** ts(2300) 重复的标志 */
type Language = {
id: number;
}
/** ts(2300) 重复的标志 */
type Language = {
name: string;
}
let lang: Language = {
id: 1,
name: 'name'
}
}
泛型
function foo<T>(name: T) {
return name
}
let a = foo<number>(1)
let b = foo(1) //可省略
function reflectArray<P>(param: P[]) {
return param;
}
const reflectArr = reflectArray([1, '1']); // reflectArr 是 (string | number)[]
泛型类
class Memory<S> {
store: S;
constructor(store: S) {
this.store = store;
}
set(store: S) {
this.store = store;
}
get() {
return this.store;
}
}
const numMemory = new Memory<number>(1); // <number> 可缺省
const getNumMemory = numMemory.get(); // 类型是 number
numMemory.set(2); // 只能写入 number 类型
const strMemory = new Memory(''); // 缺省 <string>
const getStrMemory = strMemory.get(); // 类型是 string
strMemory.set('string'); // 只能写入 string 类型
泛型入参的约束与默认值
interface ReduxModelMixed<State extends {} = { id: number; name: string }> {
state: State
}
class
类的类型
类的类型和函数类似,即在声明类的时候,其实也同时声明了一个特殊的类型(确切地讲是一个接口类型),这个类型的名字就是类名,表示类实例的类型;在定义类的时候,我们声明的除构造函数外所有属性、方法的类型就是这个特殊类型的成员
class A {
name: string;
constructor(name: string) {
this.name = name;
}
eat() {}
}
const a1: A = {}; // ts(2741) Property 'name' is missing in type '{}' but required in type 'A'.
const a2: A = { name: 'a2', eat() { } }; // ok
class中的修饰符
注意:如果只读修饰符和可见性修饰符同时出现,我们需要将只读修饰符写在可见修饰符后面
- public 修饰的是在任何地方可见、公有的属性或方法;
- private 修饰的是仅在同一类中可见、私有的属性或方法;
- protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法。
- readonly 只读修饰符
class Son {
public readonly firstName: string;
constructor(firstName: string) {
this.firstName = firstName;
}
}
const son = new Son('Tony');
抽象类
它是一种不能被实例化仅能被子类继承的特殊类。
abstract class Adder {
abstract x: number;
abstract y: number;
abstract add(): number;
displayName = 'Adder';
addTwice(): number {
return (this.x + this.y) * 2;
}
}
class NumAdder extends Adder {
x: number;
y: number;
constructor(x: number, y: number) {
super();
this.x = x;
this.y = y;
}
add(): number {
return this.x + this.y;
}
}
const numAdder = new NumAdder(1, 2);
console.log(numAdder.displayName); // => "Adder"
console.log(numAdder.add()); // => 3
console.log(numAdder.addTwice()); // => 6
实际上,我们也可以定义一个描述对象结构的接口类型抽象类的结构,并通过 implements 关键字约束类的实现。使用接口与使用抽象类相比,区别在于接口只能定义类成员的类型
abstract class A {
abstract x: number
abstract y: number
abstract add(): number;
log(): number {
return 1
}
}
interface B {
x: number;
y: number;
add: () => number;
}
类型断言
类型断言的约束条件:父子、子父类型之间可以使用类型断言进行转换。其中基础类型的字面量类型之间也可以转换
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2); // 提示 ts(2322)
// 写法1
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = arrayNumber.find(num => num > 2) as number;
// 写法2
const arrayNumber: number[] = [1, 2, 3, 4];
const greaterThan2: number = <number>arrayNumber.find(num => num > 2);
const obj = {
url:'http',
method:'get'
} as const
function request(url: sting, method: 'get'|'post') {
}
request(obj.url, obj.method)
非空断言
let mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // ts(2531)
类型谓词(is)
interface Vehicle {
move: (distance: number) => void;
}
class Car implements Vehicle {
move = (distance: number) => {
// Move car…
};
turnSteeringWheel = (direction: string) => {
// Turn wheel…
};
}
class VehicleController {
vehicle: Vehicle;
constructor(vehicle: Vehicle) {
this.vehicle = vehicle;
}
}
const anotherCar = {
move: (distance: number) => null,
turnSteeringWheel: (direction: string) => null
};
const anotherVehicleController = new VehicleController(anotherCar);
const { vehicle } = anotherVehicleController;
function isCar(p: any): p is Car {
return (p as Car).turnSteeringWheel !== undefined
}
if (isCar(vehicle)) {
vehicle.turnSteeringWheel('left');
console.log("这是一辆车");
} else {
console.log("这不是一辆车");
}
在以上代码中,我们定义了一个通用的类型保护函数,你可以在需要的时候使用它来缩窄类型。以前面自定义类型保护的例子来说,我们就可以按照以下方式来使用 isOfType 通用的类型保护函数:
function isOfType<T>(
varToBeChecked: any,
propertyToCheckFor: keyof T
): varToBeChecked is T {
return (varToBeChecked as T)[propertyToCheckFor] !== undefined;
}
// isCar(anotherCar) -> isOfType<Car>(vehicle, 'turnSteeringWheel')
if (isOfType<Car>(vehicle, 'turnSteeringWheel')) {
anotherCar.turnSteeringWheel('left');
console.log("这是一辆车");
} else {
console.log("这不是一辆车");
}
TS中的类型兼容
Number、String、Boolean、Symbol
初学 TypeScript 时,很容易和原始类型 number、string、boolean、symbol 混淆的首字母大写的 Number、String、Boolean、Symbol 类型,后者是相应原始类型的包裹对象,姑且把它们称之为对象类型。 从类型兼容性上看,原始类型兼容对应的对象类型,反过来对象类型不兼容对应的原始类型。
let num: number;
let Num: Number;
Num = num; // ok
num = Num; // ts(2322)
因此,我们需要铭记不要使用对象类型来注解值的类型,因为这没有任何意义。
object、Object 和 {}
小 object 代表的是所有非原始类型,也就是说我们不能把 number、string、boolean、symbol 原始类型赋值给 object。在严格****模式下,null 和 undefined 类型也不能赋给 object。
大Object 代表所有拥有 toString、hasOwnProperty 方法的类型,所以所有原始类型、非原始类型都可以赋给 Object。同样,在严格模式下,null 和 undefined 类型也不能赋给 Object。
{}空对象类型和大 Object 一样,也是表示原始类型和非原始类型的集合,并且在严格模式下,null 和 undefined 也不能赋给 {}
综上结论:{}、大 Object 是比小 object 更宽泛的类型(least specific),{} 和大 Object 可以互相代替,用来表示原始类型(null、undefined 除外)和非原始类型;而小 object 则表示非原始类型。
any
any类型可以赋值给除了never之外的任意其他类型,反过来其他类型也可以赋值给any.
never
never类型可以赋值给任何类型,但反过来不能被其他任何类型(包括any在内)赋值
{
let never: never = (() => {
throw Error('never');
})();
let a: number = never; // ok
let b: () => any = never; // ok
let c: {} = never; // ok
}
// 应用场景
function handle(message: string) {
switch (typeof message) {
case 'string':
break
default:
const check: never = message
}
}
unknown
unknown不能给除了any和unknown类型之外的任何其他类型赋值,但其他类型都可以赋值给unknown
{
let unknown: unknown;
const a: number = unknown; // ts(2322)
const b: () => any = unknown; // ts(2322)
const c: {} = unknown; // ts(2322)
}
void、null、undefined
void、null、undefined 这三大废材类型的兼容性也很特别,比如 void 类型仅可以赋值给 any 和 unknown 类型,反过来仅 any、never、undefined 可以赋值给 void。 在我们推崇并使用的严格模式下,null、undefined 表现出与 void 类似的兼容性,即不能赋值给除 any 和 unknown 之外的其他类型,反过来其他类型(除了 any 和 never 之外)都不可以赋值给 null 或 undefined
enum
数字枚举和数字类型相互兼容。 在如下示例中,我们在第 5 行把枚举 A 赋值给了数字(number)类型,并在第 7 行使用数字字面量 1 替代了枚举 A。
{
enum A {
one
}
let num: number = A.one; // ok
let fun = (param: A) => void 0;
fun(1); // ok
}
此外,不同枚举之间不兼容。如下示例中的第 10~11 行,因为枚举 A 和 B 不兼容,所以都会提示一个 ts(2322) 类型的错误。
{
enum A {
one
}
enum B {
one
}
let a: A;
let b: B;
a = b; // ts(2322)
b = a; // ts(2322)
}
TypeScript 中类型的兼容性都是基于结构化子类型的一般原则进行判定的。
所有的子类型与它的父类型都兼容,如下代码所示
子类型
{
const one = 1;
let num: number = one; // ok
interface IPar {
name: string;
}
interface IChild extends IPar {
id: number;
}
let Par: IPar;
let Child: IChild;
Par = Child; // ok
class CPar {
cname = '';
}
class CChild extends CPar {
cid = 1;
}
let ParInst: CPar;
let ChildInst: CChild;
ParInst = ChildInst; // ok
let mixedNum: 1 | 2 | 3 = one; // ok
}
结构类型
如果两个类型的结构一致,则它们是互相兼容的。比如拥有相同类型的属性、方法的接口类型或类,则可以互相赋值。 还有一个特殊的场景:两个接口类型或者类,如果其中一个类型不仅拥有另外一个类型全部的属性和方法,还包含其他的属性和方法(如同继承自另外一个类型的子类一样),那么前者是可以兼容后者的。
{
interface I1 {
name: string;
}
interface I2 {
id: number;
name: string;
}
class C2 {
id = 1;
name = '1';
}
let O1: I1;
let O2: I2;
let InstC2: C2;
O1 = O2;
O1 = InstC2;
}
这里涉及一个需要特别注意的特性:虽然包含多余属性 id 的变量 O2 可以赋值给变量 O1,但是如果我们直接将一个与变量 O2 完全一样结构的对象字面量赋值给变量 O1,则会提示一个 ts(2322) 类型不兼容的错误(如下示例第 2 行),这就是对象字面的 freshness 特性。
O1 = {
id: 2, // ts(2322)
name: 'name'
};
let O3 = {
id: 2,
name: 'name'
};
O1 = O3; // ok
O1 = {
id: 2,
name: 'name'
} as I2; // ok
也就是说一个对象字面量没有被变量接收时,它将处于一种 freshness 新鲜的状态。这时 TypeScript 会对对象字面量的赋值操作进行严格的类型检测,只有目标变量的类型与对象字面量的类型完全一致时,对象字面量才可以赋值给目标变量,否则会提示类型错误。 当然,我们也可以通过使用变量接收对象字面量或使用类型断言解除 freshness
可继承和可实现
类型兼容性还决定了接口类型和类是否可以通过 extends 继承另外一个接口类型或者类,以及类是否可以通过 implements 实现接口。
{
interface I1 {
name: number;
}
interface I2 extends I1 { // ts(2430) 因为接口类型 I1 和接口类型 I2 包含不同类型的 name 属性不兼容,所以接口类型 I2 不能继承接口类型 I1。
name: string;
}
class C1 {
name = '1';
private id = 1;
}
class C2 extends C1 { // ts(2415) 因为类 C1 和类 C2 不满足类兼容条件,所以类 C2 也不能继承类 C1。
name = '2';
private id = 1;
}
class C3 implements I1 {
name = ''; // ts(2416) 因为接口类型 I1 和类 C3 包含不同类型的 name 属性,所以类 C3 不能实现接口类型 I1。
}
}
泛型类型、泛型类
泛型类型、泛型类的兼容性实际指的是将它们实例化为一个确切的类型后的兼容性。
{
interface I1<T> {
id: number;
}
let O1: I1<string>;
let O2: I1<number>;
O1 = O2; // ol 因为接口泛型 I1 的入参 T 是无用的,且实例化类型 I1<string> 和 I1<numer> 的结构一致,即类型兼容,所以对应的变量 O2 可以给变量 O1赋值
}
对于未明确指定类型入参泛型的兼容性,例如函数泛型(实际上仅有函数泛型才可以在不需要实例化泛型的情况下赋值),TypeScript 会把 any 类型作为所有未明确指定的入参类型实例化泛型,然后再检测其兼容性
{
let fun1 = <T>(p1: T): 1 => 1;
let fun2 = <T>(p2: T): number => 2;
fun2 = fun1; // ok?
}
变型
TypeScript 中的变型指的是根据类型之间的子类型关系推断基于它们构造的更复杂类型之间的子类型关系。比如根据 Dog 类型是 Animal 类型子类型这样的关系,我们可以推断数组类型 Dog[] 和 Animal[] 、函数类型 () => Dog 和 () => Animal 之间的子类型关系。
变型描述的就是基于 Dog 和 Animal 之间的子类型关系,从而得出 F(Dog) 和 F(Animal) 之间的子类型关系的一般性质。而这个性质体现为子类型关系可能会被保持、反转、忽略,因此它可以被划分为协变、逆变、双向协变和不变这 4 个专业术语。
协变
协变也就是说如果 Dog 是 Animal 的子类型,则 F(Dog) 是 F(Animal) 的子类型,这意味着在构造的复杂类型中保持了一致的子类型关系,下面举个简单的例子:
{
type isChild<Child, Par> = Child extends Par ? true : false;
interface Animal {
name: string;
}
interface Dog extends Animal {
woof: () => void;
}
type Covariance<T> = T;
type isCovariant = isChild<Covariance<Dog>, Covariance<Animal>>; // true
}
type isPropAssignmentCovariant = isChild<{ type: Dog }, { type: Animal }>; // true
type isArrayElementCovariant = isChild<Dog[], Animal[]>; // true
type isReturnTypeCovariant = isChild<() => Dog, () => Animal>; // true
逆变
逆变也就是说如果 Dog 是 Animal 的子类型,则 F(Dog) 是 F(Animal) 的父类型,这与协变正好反过来。
实际场景中,在我们推崇的 TypeScript 严格模式下,函数参数类型是逆变的,具体示例如下:
type Contravariance<T> = (param: T) => void;
type isNotContravariance = isChild<Contravariance<Dog>, Contravariance<Animal>>; // false;
type isContravariance = isChild<Contravariance<Animal>, Contravariance<Dog>>; // true;
为了更易于理解,我们可以从安全性的角度理解函数参数是逆变的设定。
如果函数参数类型是协变而不是逆变,那么意味着函数类型 (param: Dog) => void 和 (param: Animal) => void 是兼容的,这与 Dog 和 Animal 的兼容一致,所以我们可以用 (param: Dog) => void 代替 (param: Animal) => void 遍历 Animal[] 类型数组。
但是,这样是不安全的,因为它不能确保 Animal[] 数组中的成员都是 Dog(可能混入 Animal 类型的其他子类型,比如 Cat),这就会导致 (param: Dog) => void 类型的函数可能接收到 Cat 类型的入参。
const visitDog = (animal: Dog) => {
animal.woof();
};
let animals: Animal[] = [{ name: 'Cat', miao: () => void 0, }];
animals.forEach(visitDog); // ts(2345)
在示例中,如果函数参数类型是协变的,那么第 5 行就可以通过静态类型检测,而不会提示一个 ts(2345) 类型的错误。这样第 1 行定义的 visitDog 函数在运行时就能接收到 Dog 类型之外的入参,并调用不存在的 woof 方法,从而在运行时抛出错误。
正是因为函数参数是逆变的,所以使用 visitDog 函数遍历 Animal[] 类型数组时,在第 5 行提示了类型错误,因此也就不出现 visitDog 接收到一只 cat 的情况。
假设: C extends B extends A,如下图,我们记成C < B < A
参数是输入的类型,返回值是输出的类型。输入时,蓝色椭圆必须要比绿色椭圆大,才能保证能够使用输入的类型;输出时,蓝色必须要比绿色的小,才能保证输出的类型能够被使用。按照箭头的方向看,本质上都是:将小类型向大类型进行“投射”,即箭头尾部的类型,是箭头头部类型的子类型。如上图,参数是B->A, 返回值是C->B
双向协变
双向协变也就是说如果 Dog 是 Animal 的子类型,则 F(Dog) 是 F(Animal) 的子类型,也是父类型,既是协变也是逆变。
对应到实际的场景,在 TypeScript 非严格模式下,函数参数类型就是双向协变的。如前边提到函数只有在参数是逆变的情况下才安全,且本文一直在强调使用严格模式,所以双向协变并不是一个安全或者有用的特性,因此我们不大可能遇到这样的实际场景。
但在某些资料中有提到,如果函数参数类型是双向协变,那么它是有用的,并进行了举例论证 (以下示例缩减自网络) :
interface Event {
timestamp: number;
}
interface MouseEvent extends Event {
x: number;
y: number;
}
function addEventListener(handler: (n: Event) => void) {}
addEventListener((e: MouseEvent) => console.log(e.x + ',' + e.y)); // ts(2769)
这种方式确实方便了很多,但是并不安全,原因见前边 Dog 和 Cat 的示例。而且在严格模式下,参数类型是逆变而不是双向协变的,所以 ts(2769) 的错误。
由此可以得出,真正有用且安全的做法是使用泛型,如下所示:
function addEventListener<E extends Event>(handler: (n: E) => void) {}
addEventListener((e: MouseEvent) => console.log(e.x + ',' + e.y)); // ok
在示例中的第 1 行,因为我们重新定义了带约束条件泛型入参的 addEventListener,它可以传递任何参数类型是 Event 子类型的函数作为入参,所以在第 2 行传入参数类型是 MouseEvent 的箭头函数作为入参时,则不会提示类型错误
不变
不变即只要是不完全一样的类型,它们一定是不兼容的。也就是说即便 Dog 是 Animal 的子类型,如果 F(Dog) 不是 F(Animal) 的子类型,那么 F(Animal) 也不是 F(Dog) 的子类型。 对应到实际场景,出于类型安全层面的考虑,在特定情况下我们可能希望数组是不变的(实际上是协变),见示例:
interface Cat extends Animal {
miao: () => void;
}
const cat: Cat = {
name: 'Cat',
miao: () => void 0,
};
const dog: Dog = {
name: 'Dog',
woof: () => void 0,
};
let dogs: Dog[] = [dog];
animals = dogs; // ok
animals.push(cat); // ok
dogs.forEach(visitDog); // 类型 ok,但运行时会抛出错误
因此,对于可变的数组而言,不变似乎是更安全、合理的设定。不过,在 TypeScript 中可变、不变的数组都是协变的,这是需要我们注意的一个陷阱。
函数类型兼容性
因为函数类型的兼容性、子类型关系有着更复杂的考量(它还需要结合参数和返回值的类型进行确定),所以下面我们详细介绍一下函数类型兼容性的一般规则。
(1)返回值
前边我们已经讲过返回值类型是协变的,所以在参数类型兼容的情况下,函数的子类型关系与返回值子类型关系一致。也就是说返回值类型兼容,则函数兼容。
(2)参数类型
前边我们也讲过参数类型是逆变的,所以在参数个数相同、返回值类型兼容的情况下,函数子类型关系与参数子类型关系是反过来的(逆变)。
(3)参数个数
在索引位置相同的参数和返回值类型兼容的前提下,函数兼容性取决于参数个数,参数个数少的兼容个数多,下面我们看一个具体的示例:
{
let lessParams = (one: number) => void 0;
let moreParams = (one: number, two: string) => void 0;
lessParams = moreParams; // ts(2322)
moreParams = lessParams; // ok
}
在示例中,lessParams 参数个数少于 moreParams,所以如第 4 行所示 lessParams 和 moreParams 兼容,并可以赋值给 moreParams。
注意:如果你觉得参数个数少的函数兼容参数个数多的函数不好理解,那么可以试着从安全性角度理解(是参数少的函数赋值给参数多的函数安全,还是参数多的函数赋值给参数少的函数安全),这里限于篇幅有限就不展开了。
(4)可选和剩余参数
可选参数可以兼容剩余参数、不可选参数;不可选参数和剩余参数是互相兼容的;下面我们具体看一个示例:
let optionalParams = (one?: number, tow?: number) => void 0;
let requiredParams = (one: number, tow: number) => void 0;
let restParams = (...args: number[]) => void 0;
requiredParams = optionalParams; // ok
restParams = optionalParams; // ok
optionalParams = restParams; // ts(2322)
optionalParams = requiredParams; // ts(2322)
restParams = requiredParams; // ok 把不可选参数 requiredParams 赋值给剩余参数 restParams 其实是不安全的(但是符合类型检测),我们需要从方便性上理解这个设定。
requiredParams = restParams; // ok 从安全性的角度理解第 9 行是安全的,所以可以赋值。
正是基于不可选参数和剩余参数是互相兼容的这个设定,我们才可以将剩余参数类型函数定义为其他所有参数类型函数的父类型,并用来约束其他类型函数的类型范围,比如说在泛型中约束函数类型入参的范围。
type GetFun<F extends (...args: number[]) => any> = Parameters<F>; //我们使用剩余参数函数类型 (...args: number[]) => any 约束了入参 F 的类型
//以下传入的函数类型入参都是这个剩余参数函数类型的子类型。
type GetRequiredParams = GetFun<typeof requiredParams>;
type GetRestParams = GetFun<typeof restParams>;
type GetEmptyParams = GetFun<() => void>;
增强类型系统
在 TypeScript 中预留了一个增强类型的口子,使得我们可以方便地扩展原来的类型系统,以兼容 JavaScript 的代码。 通过使用 declare 关键字,我们可以声明全局的变量、方法、类、对象。
需要注意:使用 declare关键字时,我们不需要编写声明的变量、函数、类的具体实现(因为变量、函数、类在其他库中已经实现了),只需要声明其类型即可
declare变量
declare var val1: string;
val1 = '1';
declare function toString(x: number): string;
const x = toString(1); // => string
declare class Person {
public name: string;
private age: number;
constructor(name: string);
getAge(): number;
}
const person = new Person('Mike');
declare enum Direction {
Up,
Down,
Left,
Right,
}
const directions = [Direction.Up, Direction.Down, Direction.Left, Direction.Right];
declare 模块
TypeScript 与 ES6 一样,任何包含顶级 import 或 export 的文件都会被当作一个模块。我们可以通过声明模块类型,为缺少 TypeScript 类型定义的三方库或者文件补齐类型定义,如下示例:
声明模块的语法:
declare module '模块名' {}。在模块声明的内部,我们只需要使用 export 导出对应库的类、函数即可。
declare 文件
// lodash.d.ts
declare module 'lodash' {
export function first<T extends unknown>(array: T[]): T;
}
// index.ts
import { first } from 'lodash';
first([1, 2, 3]); // => number;
declare 文件
在使用 TypeScript 开发前端应用时,我们可以通过 import 关键字导入文件,比如先使用 import 导入图片文件,再通过 webpack 等工具处理导入的文件。
但是,因为 TypeScript 并不知道我们通过 import 导入的文件是什么类型,所以需要使用 declare 声明导入的文件类型,下面看一个具体的示例:
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.png' {
const src: string;
export default src;
}
在上面的例子中,我们使用了 *.xxx 模块通配符匹配一类文件。
这里标记的图片文件的默认导出的类型是 string ,通过 import 使用图片资源时,TypeScript 会将导入的图片识别为 string 类型,因此也就可以把 import 的图片赋值给 的 src 属性,因为它们的类型都是 string,是匹配的。
declare namespace
同于声明模块,命名空间一般用来表示具有很多子属性或者方法的全局对象变量。
我们可以将声明命名空间简单看作是声明一个更复杂的变量,如下示例:
declare namespace $ {
const version: number;
function ajax(settings?: any): void;
}
$.version; // => number
$.ajax();
类型增强
在 TypeScript 中,如果文件包含顶层的 export 或者 import,则会被当作 module,在 module 中定义的、没有显式 export 的变量、函数、类对外都不可见;相反,如果文件不包含顶层的 export 或者 import,则会被当作 script,script 里的内容(类型声明、变量声明)都是全局可见的(对 module 也是可见的)。
这就是为什么使用同样的语法进行人工补齐类型声明时,有的类型声明在其他的模块、文件中无需显式 import 就可以直接使用,而有的类型声明必须显式 import 之后才可以使用。
如果我们确实想让 module 中的类型全局可见,则可以使用 declare global 声明全局类型,如下示例:
// myGlobalModule.ts
declare global {
type GlobalUserId = number;
}
类型合并
在 TypeScript 中,相同的接口、命名空间会依据一定的规则进行合并。
合并接口
最简单、常见的声明合并是接口合并 对于函数成员而言,每个同名的函数声明都会被当作这个函数的重载。 需要注意的是后面声明的接口具有更高的优先级
interface Person {
identity(val: any): any;
name: string;
}
interface Person {
identity(val: boolean): boolean;
age: number;
}
// 相当于
interface Person {
identity(val: boolean): boolean;
identity(val: any): any;
name: string;
age: number;
}
合并 namespace
合并 namespace 与合并接口类似,命名空间的合并也会合并其导出成员的属性。不同的是,非导出成员仅在原命名空间内可见。
namespace Person {
const age = 18;
export function getAge() {
return age;
}
}
namespace Person {
export function getMyAge() {
return age; // TS2304: Cannot find name 'age'.
}
}
扩充模块
// person.ts
export class Person {}
// index.ts
import { Person } from './person';
declare module './person' {
interface Person {
greet: () => void;
}
}
Person.prototype.greet = () => {
console.log('Hi!');
};