作为一门强大的静态类型检查工具,如今在许多中大型应用程序以及流行的JS库中均能看到TypeScript的身影。JS作为一门弱类型语言,在我们写代码的过程中稍不留神便会修改掉变量的类型,从而导致一些出乎意料的运行时错误。然而TypeScript在编译过程中便能帮我们解决这个难题,不仅在JS中引入了强类型检查,并且编译后的JS代码能够运行在任何浏览器环境,Node环境和任何支持ECMAScript 3(或更高版本)的JS引擎中。最近公司刚好准备使用TypeScript来对现有系统进行重构,以前使用TypeScript的机会也不多,特别是一些有用的高级用法,所以借着这次机会,重新巩固夯实一下这方面的知识点,如果有错误的地方,还请指出。
在ES5中,我们一般通过函数或者基于原型的继承来封装一些组件公共的部分方便复用,然而在TypeScript中,我们可以像类似Java语言中以面向对象的方式使用类继承来创建可复用的组件。我们可以通过class关键字来创建类,并基于它使用new操作符来实例化一个对象。为了将多个类的公共部分进行抽象,我们可以创建一个父类并让子类通过extends关键字来继承父类,从而减少一些冗余代码的编写增加代码的可复用性和可维护性。示例如下:
class Parent {
readonly x: number;
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
}
class Child extends Parent {
readonly y: number;
constructor() {
// 注意此处必须优先调用super()方法
super();
this.y = 2;
}
print() {
// 通过super调用父类原型上的方法,但是方法中的this指向的是子类的实例
super.print();
console.log(this.y);
}
}
const child = new Child();
console.log(child.print()) // -> 1 2
在上述示例中,Child子类中对父类的print方法进行重写,同时在内部使用super.print()来调用父类的公共逻辑,从而实现逻辑复用。class关键字作为构造函数的语法糖,在经过TypeScript编译后,最终会被转换为兼容性好的浏览器可识别的ES5代码。class在面向对象的编程范式中非常常见,因此为了弄清楚其背后的实现机制,我们不妨多花点时间来看下经过编译转换之后的代码是什么样子的(当然这部分已经比较熟悉的同学可以直接跳过)。
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
}
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var Parent = /** @class */ (function () {
function Parent() {
this.x = 1;
}
Parent.prototype.print = function () {
console.log(this.x);
};
return Parent;
}());
var Child = /** @class */ (function (_super) {
__extends(Child, _super);
function Child() {
var _this =
// 注意此处必须优先调用super()方法
_super.call(this) || this;
_this.y = 2;
return _this;
}
Child.prototype.print = function () {
// 通过super调用父类原型上的方法,但是方法中的this指向的是子类的实例
_super.prototype.print.call(this);
console.log(this.y);
};
return Child;
}(Parent));
var child = new Child();
console.log(child.print()); // -> 1 2
以上就是转换后的完整代码,为了方便对比,这里将原来的注释信息保留,仔细研究这段代码我们会发现以下几个要点:
1) 子类Child的构造函数中super()方法被转换成了var _this = _super.call(this) || this,这里的_super指的就是父类Parent,因此这句代码的含义就是调用父类构造函数并将this绑定到子类的实例上,这样的话子类实例便可拥有父类的x属性。因此为了实现属性继承,我们必须在子类构造函数中调用super()方法,如果不调用会编译不通过。
2) 子类Child的print方法中super.print()方法被转换成了_super.prototype.print.call(this),这句代码的含义就是调用父类原型上的print方法并将方法中的this指向子类实例,由于在上一步操作中我们已经继承到父类的x属性,因此这里我们将直接打印出子类实例的x属性的值。
3) extends关键字最终被转换为__extends(Child, _super)方法,其中_super指的是父类Parent,为了方便查看,这里将_extends方法单独提出来进行研究。
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
}
return function (d, b) {
// 第一部分
extendStatics(d, b);
// 第二部分
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
在以上代码中,主要可以分为两个部分来进行理解,第一部分为extendStatics(d, b)方法,第二部分为该方法后面的两行代码。
第一部分:
在extendStatics方法内部虽然代码量相对较多,但是不难发现其实还是主要为了兼容ES5版本的执行环境。在ES6中新增了Object.setPrototypeOf方法用于手动设置对象的原型,但是在ES5的环境中我们一般通过一个非标准的__proto__属性来进行设置,Object.setPrototypeOf方法的原理其实也是通过该属性来设置对象的原型,其实现方式如下:
Object.setPrototypeOf = function(obj, proto) {
obj.__proto__ = proto;
return obj;
}
在extendStatics(d, b)方法中,d指子类Child,b指父类Parent,因此该方法的作用可以解释为:
// 将子类Child的__proto__属性指向父类Parent
Child.__proto__ = Parent;
可以将这行代码理解为构造函数的继承,或者叫静态属性和静态方法的继承,即属性和方法不是挂载到构造函数的prototype原型上的,而是直接挂载到构造函数本身,因为在JS中函数本身也可以作为一个对象,并可以为其赋予任何其他的属性,示例如下:
function Foo() {
this.x = 1;
this.y = 2;
}
Foo.bar = function() {
console.log(3);
}
Foo.baz = 4;
console.log(Foo.bar()) // -> 3
console.log(Foo.baz) // -> 4
因此当我们在子类Child中以Child.someProperty访问属性时,如果子类中不存在就会通过Child.__proto__寻找父类的同名属性,通过这种方式来实现静态属性和静态方法的路径查找。
第二部分:
在第二部分中仅包含以下两行代码:
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
其中d指子类Child,b指父类Parent,这里对于JS中实现继承的几种方式比较熟悉的同学可以一眼看出,这里使用了寄生组合式继承的方式,通过借用一个中间函数__()来避免当修改子类的prototype上的方法时对父类的prototype所造成的影响。我们知道,在JS中通过构造函数实例化一个对象之后,该对象会拥有一个__proto__属性并指向其构造函数的prototype属性,示例如下:
function Foo() {
this.x = 1;
this.y = 2;
}
const foo = new Foo();
foo.__proto__ === Foo.prototype; // -> true
对于本例中,如果通过子类Child来实例化一个对象之后,会产生如下关联:
const child = new Child();
child.__proto__ === (Child.prototype = new __());
child.__proto__.__proto__ === __.prototype === Parent.prototype;
// 上述代码等价于下面这种方式
Child.prototype.__proto__ === Parent.prototype;
因此当我们在子类Child的实例child对象中通过child.someMethod()调用某个方法时,如果在实例中不存在该方法,则会沿着__proto__继续往上查找,最终会经过父类Parent的prototype原型,即通过这种方式来实现方法的继承。
基于对以上两个部分的分析,我们可以总结出以下两点:
// 表示构造函数的继承,或者叫做静态属性和静态方法的继承,总是指向父类
1. Child.__proto__ === Parent;
// 表示方法的继承,总是指向父类的prototype属性
2. Child.prototype.__proto__ === Parent.prototype;
TypeScript为我们提供了访问修饰符(Access Modifiers)来限制在class外部对内部属性的访问,访问修饰符主要包含以下三种:
我们通过一些示例来对几种修饰符进行对比:
class Human {
public name: string;
public age: number;
public constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
const man = new Human('tom', 20);
console.log(man.name, man.age); // -> tom 20
man.age = 21;
console.log(man.age); // -> 21
在上述示例中,由于我们将访问修饰符设置为public,因此我们通过实例man来访问name和age属性是被允许的,同时对age属性重新赋值也是允许的。但是在某些情况下,我们希望某些属性是对外不可见的,同时不允许被修改,那么我们就可以使用private修饰符:
class Human {
public name: string;
private age: number; // 此处修改为使用private修饰符
public constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
const man = new Human('tom', 20);
console.log(man.name); // -> tom
console.log(man.age);
// -> Property 'age' is private and only accessible within class 'Human'.
我们将age属性的修饰符修改为private后,在外部通过man.age对其进行访问,TypeScript在编译阶段就会发现其是一个私有属性并最终将会报错。
注意:在TypeScript编译之后的代码中并没有限制对私有属性的存取操作。
编译后的代码如下:
var Human = /** @class */ (function () {
function Human(name, age) {
this.name = name;
this.age = age;
}
return Human;
}());
var man = new Human('tom', 20);
console.log(man.name); // -> tom
console.log(man.age); // -> 20
使用private修饰符修饰的属性或者方法在子类中也是不允许访问的,示例如下:
class Human {
public name: string;
private age: number;
public constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
class Woman extends Human {
private gender: number = 0;
public constructor(name: string, age: number) {
super(name, age);
console.log(this.age);
}
}
const woman = new Woman('Alice', 18);
// -> Property 'age' is private and only accessible within class 'Human'.
在上述示例中由于在父类Human中age属性被设置为private,因此在子类Woman中无法访问到age属性,为了让在子类中允许访问age属性,我们可以使用protected修饰符来对其进行修饰:
class Human {
public name: string;
protected age: number; // 此处修改为使用protected修饰符
public constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
class Woman extends Human {
private gender: number = 0;
public constructor(name: string, age: number) {
super(name, age);
console.log(this.age);
}
}
const woman = new Woman('Alice', 18); // -> 18
当我们将private修饰符用于构造函数时,则表示该类不允许被继承或实例化,示例如下:
class Human {
public name: string;
public age: number;
// 此处修改为使用private修饰符
private constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
class Woman extends Human {
private gender: number = 0;
public constructor(name: string, age: number) {
super(name, age);
}
}
const man = new Human('Alice', 18);
// -> Cannot extend a class 'Human'. Class constructor is marked as private.
// -> Constructor of class 'Human' is private and only accessible within the class declaration.
当我们将protected修饰符用于构造函数时,则表示该类只允许被继承,示例如下:
class Human {
public name: string;
public age: number;
// 此处修改为使用protected修饰符
protected constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
class Woman extends Human {
private gender: number = 0;
public constructor(name: string, age: number) {
super(name, age);
}
}
const man = new Human('Alice', 18);
// -> Constructor of class 'Human' is protected and only accessible within the class declaration.
另外我们还可以直接将修饰符放到构造函数的参数中,示例如下:
class Human {
// public name: string;
// private age: number;
public constructor(public name: string, private age: number) {
this.name = name;
this.age = age;
}
}
const man = new Human('tom', 20);
console.log(man.name); // -> tom
console.log(man.age);
// -> Property 'age' is private and only accessible within class 'Human'.
当我们的项目中拥有很多不同的类时并且这些类之间可能存在某方面的共同点,为了描述这种共同点,我们可以将其提取到一个接口(interface)中用于集中维护,并使用implements关键字来实现这个接口,示例如下:
interface IHuman {
name: string;
age: number;
walk(): void;
}
class Human implements IHuman {
public constructor(public name: string, public age: number) {
this.name = name;
this.age = age;
}
walk(): void {
console.log('I am walking...');
}
}
上述代码在编译阶段能顺利通过,但是我们注意到在Human类中包含constructor构造函数,如果我们想在接口中为该构造函数定义一个签名并让Human类来实现这个接口,看会发生什么:
interface HumanConstructor {
new (name: string, age: number);
}
class Human implements HumanConstructor {
public constructor(public name: string, public age: number) {
this.name = name;
this.age = age;
}
walk(): void {
console.log('I am walking...');
}
}
// -> Class 'Human' incorrectly implements interface 'HumanConstructor'.
// -> Type 'Human' provides no match for the signature 'new (name: string, age: number): any'.
然而TypeScript会编译出错,告诉我们错误地实现了HumanConstructor接口,这是因为当一个类实现一个接口时,只会对实例部分进行编译检查,类的静态部分是不会被编译器检查的。因此这里我们尝试换种方式,直接操作类的静态部分,示例如下:
interface HumanConstructor {
new (name: string, age: number);
}
interface IHuman {
name: string;
age: number;
walk(): void;
}
class Human implements IHuman {
public constructor(public name: string, public age: number) {
this.name = name;
this.age = age;
}
walk(): void {
console.log('I am walking...');
}
}
// 定义一个工厂方法
function createHuman(constructor: HumanConstructor, name: string, age: number): IHuman {
return new constructor(name, age);
}
const man = createHuman(Human, 'tom', 18);
console.log(man.name, man.age); // -> tom 18
在上述示例中通过额外创建一个工厂方法createHuman并将构造函数作为第一个参数传入,此时当我们调用createHuman(Human, 'tom', 18)时编译器便会检查第一个参数是否符合HumanConstructor接口的构造器签名。
在声明合并中最常见的合并类型就是接口了,因此这里先从接口开始介绍几种比较常见的合并方式。
示例代码如下:
interface A {
name: string;
}
interface A {
age: number;
}
// 等价于
interface A {
name: string;
age: number;
}
const a: A = {name: 'tom', age: 18};
接口合并的方式比较容易理解,即声明多个同名的接口,每个接口中包含不同的属性声明,最终这些来自多个接口的属性声明会被合并到同一个接口中。
注意:所有同名接口中的非函数成员必须唯一,如果不唯一则必须保证类型相同,否则编译器会报错。对于函数成员,后声明的同名接口会覆盖掉之前声明的同名接口,即后声明的同名接口中的函数相当于一次重载,具有更高的优先级。
函数的合并可以简单理解为函数的重载,即通过同时定义多个不同类型参数或不同类型返回值的同名函数来实现,示例代码如下:
// 函数定义
function foo(x: number): number;
function foo(x: string): string;
// 函数具体实现
function foo(x: number | string): number | string {
if (typeof x === 'number') {
return (x).toFixed(2);
}
return x.substring(0, x.length - 1);
}
在上述示例中,我们对foo函数进行多次定义,每次定义的函数参数类型不同,返回值类型不同,最后一次为函数的具体实现,在实现中只有在兼容到前面的所有定义时,编译器才不会报错。
注意:TypeScript编译器会优先从最开始的函数定义进行匹配,因此如果多个函数定义存在包含关系,则需要将最精确的函数定义放到最前面,否则将始终不会被匹配到。
类型别名联合与接口合并有所区别,类型别名不会新建一个类型,只是创建一个新的别名来对多个类型进行引用,同时不能像接口一样被实现(implements)和继承(extends),示例如下:
type HumanProperty = {
name: string;
age: number;
gender: number;
};
type HumanBehavior = {
eat(): void;
walk(): void;
}
type Human = HumanProperty & HumanBehavior;
let woman: Human = {
name: 'tom',
age: 18,
gender: 0,
eat() {
console.log('I can eat.');
},
walk() {
console.log('I can walk.');
}
}
class HumanComponent extends Human {
constructor(public name: string, public age: number, public gender: number) {
this.name = name;
this.age = age;
this.gender = gender;
}
eat() {
console.log('I can eat.');
}
walk() {
console.log('I can walk.');
}
}
// -> 'Human' only refers to a type, but is being used as a value here.
在TypeScript中的keyof有点类似于JS中的Object.keys()方法,但是区别在于前者遍历的是类型中的字符串索引,后者遍历的是对象中的键名,示例如下:
interface Rectangle {
x: number;
y: number;
width: number;
height: number;
}
type keys = keyof Rectangle;
// 等价于
type keys = "x" | "y" | "width" | "height";
// 这里使用了泛型,强制要求第二个参数的参数名必须包含在第一个参数的所有字符串索引中
function getRectProperty<T extends object, K extends keyof T>(rect: T, property: K): T[K] {
return rect[property];
}
let rect: Rectangle = {
x: 50,
y: 50,
width: 100,
height: 200
};
console.log(getRectProperty(rect, 'width')); // -> 100
console.log(getRectProperty(rect, 'notExist'));
// -> Argument of type '"notExist"' is not assignable to parameter of type '"width" | "x" | "y" | "height"'.
在上述示例中我们通过使用keyof来限制函数的参数名property必须被包含在类型Rectangle的所有字符串索引中,如果没有被包含则编译器会报错,可以用来在编译时检测对象的属性名是否书写有误。
在某些情况下,我们希望类型中的所有属性都不是必需的,只有在某些条件下才存在,我们就可以使用Partial来将已声明的类型中的所有属性标识为可选的,示例如下:
// 该类型已内置在TypeScript中
type Partial<T> = {
[P in keyof T]?: T[P]
};
interface Rectangle {
x: number;
y: number;
width: number;
height: number;
}
type PartialRectangle = Partial<Rectangle>;
// 等价于
type PartialRectangle = {
x?: number;
y?: number;
width?: number;
height?: number;
}
let rect: PartialRectangle = {
width: 100,
height: 200
};
在上述示例中由于我们使用Partial将所有属性标识为可选的,因此最终rect对象中虽然只包含width和height属性,但是编译器依旧没有报错,当我们不能明确地确定对象中包含哪些属性时,我们就可以通过Partial来声明。
在某些应用场景下,我们可能需要从一个已声明的类型中抽取出一个子类型,在子类型中包含父类型中的部分或全部属性,这时我们可以使用Pick来实现,示例代码如下:
// 该类型已内置在TypeScript中
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
};
interface User {
id: number;
name: string;
age: number;
gender: number;
email: string;
}
type PickUser = Pick<User, "id" | "name" | "gender">;
// 等价于
type PickUser = {
id: number;
name: string;
gender: number;
};
let user: PickUser = {
id: 1,
name: 'tom',
gender: 1
};
在上述示例中,由于我们只关心user对象中的id,name和gender是否存在,其他属性不做明确规定,因此我们就可以使用Pick从User接口中拣选出我们关心的属性而忽略其他属性的编译检查。
never表示的是那些永不存在的值的类型,比如在函数中抛出异常或者无限循环,never类型可以是任何类型的子类型,也可以赋值给任何类型,但是相反却没有一个类型可以作为never类型的子类型,示例如下:
// 函数抛出异常
function throwError(message: string): never {
throw new Error(message);
}
// 函数自动推断出返回值为never类型
function reportError(message: string) {
return throwError(message);
}
// 无限循环
function loop(): never {
while(true) {
console.log(1);
}
}
// never类型可以是任何类型的子类型
let n: never;
let a: string = n;
let b: number = n;
let c: boolean = n;
let d: null = n;
let e: undefined = n;
let f: any = n;
// 任何类型都不能赋值给never类型
let a: string = '123';
let b: number = 0;
let c: boolean = true;
let d: null = null;
let e: undefined = undefined;
let f: any = [];
let n: never = a;
// -> Type 'string' is not assignable to type 'never'.
let n: never = b;
// -> Type 'number' is not assignable to type 'never'.
let n: never = c;
// -> Type 'true' is not assignable to type 'never'.
let n: never = d;
// -> Type 'null' is not assignable to type 'never'.
let n: never = e;
// -> Type 'undefined' is not assignable to type 'never'.
let n: never = f;
// -> Type 'any' is not assignable to type 'never'.
与Pick相反,Pick用于拣选出我们需要关心的属性,而Exclude用于排除掉我们不需要关心的属性,示例如下:
// 该类型已内置在TypeScript中
// 这里使用了条件类型(Conditional Type),和JS中的三目运算符效果一致
type Exclude<T, U> = T extends U ? never : T;
interface User {
id: number;
name: string;
age: number;
gender: number;
email: string;
}
type keys = keyof User; // -> "id" | "name" | "age" | "gender" | "email"
type ExcludeUser = Exclude<keys, "age" | "email">;
// 等价于
type ExcludeUser = "id" | "name" | "gender";
在上述示例中我们通过在ExcludeUser中传入我们不需要关心的age和email属性,Exclude会帮助我们将不需要的属性进行剔除,留下的属性id,name和gender即为我们需要关心的属性。一般来说,Exclude很少单独使用,可以与其他类型配合实现更复杂更有用的功能。
在上一个用法中,我们使用Exclude来排除掉其他不需要的属性,但是在上述示例中的写法耦合度较高,当有其他类型也需要这样处理时,就必须再实现一遍相同的逻辑,不妨我们再进一步封装,隐藏这些底层的处理细节,只对外暴露简单的公共接口,示例如下:
// 使用Pick和Exclude组合实现
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
interface User {
id: number;
name: string;
age: number;
gender: number;
email: string;
}
// 表示忽略掉User接口中的age和email属性
type OmitUser = Omit<User, "age" | "email">;
// 等价于
type OmitUser = {
id: number;
name: string;
gender: number;
};
let user: OmitUser = {
id: 1,
name: 'tom',
gender: 1
};
在上述示例中,我们需要忽略掉User接口中的age和email属性,则只需要将接口名和属性传入Omit即可,对于其他类型也是如此,大大提高了类型的可扩展能力,方便复用。
在本文中总结了几种TypeScript的使用技巧,如果在我们的TypeScript项目中发现有很多类型声明的地方具有共性,那么不妨可以使用文中的几种技巧来对其进行优化改善,增加代码的可维护性和可复用性。
原文:https://github.com/qq591468061/xwfe/issues/10
近些日子,我使用了新语言编程,从JavaScript,切确地说是Elm,转成TypeScript。在本文中,我将继续深挖一些我非常喜欢的TypeScript特性。
TypeScript 和 JavaScript 是目前项目开发中较为流行的两种脚本语言,我们已经熟知 TypeScript 是 JavaScript 的一个超集,但是 TypeScript 与 JavaScript 之间又有什么样的区别呢?
Nerv_是一款由京东凹凸实验室打造的类 React 前端框架,基于虚拟 DOM 技术的 JavaScript(TypeScript) 库。它基于React标准,提供了与 React 16 一致的使用方式与 API。
交叉类型:将多个类型合并为一个类型、联合类型:表示取值可以为多种类型中的一种、混合类型:一个例子就是,一个对象可以同时做为函数和对象使用,并带有额外的属性、类型断言:可以用来手动指定一个值的类型
在做比较大的,多人合作的项目的时候,TypeScript会更加地适合,这得益于它的可读性,面向对象性以及易于重构的特点。但如果只是自己做小程序,不需要太多人参与的时候,JavaScript则会更加简单。
有两种方式安装TypeScript,如何创建第一个TypeScript文件,在TypeScript中,可以使用interface来描述一个对象有firstName和lastName两个属性,TypeScript支持JavaScript的新功能,其中很重要的一个功能就是基于类的面向对象编程
使用TypeScript已经有了一段时间,这的确是一个好东西,虽说在使用的过程中也发现了一些bug,不过都是些小问题,所以整体体验还是很不错的。有关TypeScript声明类型声明相关的目前就总结了这些比较常用的
谷歌在很早之前就张开双臂拥抱 Web 应用程序,Gmail 已经发布 14 年了。当时,JavaScript 的世界是疯狂的。Gmail 工程师不得不为 IE 糟糕的垃圾回收算法捏一把汗,他们需要手动将字符串文字从 for 循环中提取出来,以避免 GC 停顿
TypeScript是一种由微软开发的自由和开源的编程语言。它是JavaScript的一个超集,TypeScript是JavaScript类型的超集,它可以编译成纯JavaScript。TypeScript可以在任何浏览器、任何计算机和任何操作系统上运行,并且是开源的。
差不多两年前,我在一个创业团队中开始了一个全新的项目。用到的全都是类似Microservices,docker,react,redux这些时髦的东西。我在前端技术方面积累了一些类似的经验
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!