TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。
TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 2015 年的 ECMAScript 和未来的提案中的特性,比如异步功能和 Decorators,以帮助建立健壮的组件。
如何在 window 对象上显式设置属性
对于使用过 JavaScript
的开发者来说,对于 window.MyNamespace = window.MyNamespace || {};
这行代码并不会陌生。为了避免开发过程中出现冲突,我们一般会为某些功能设置独立的命名空间。
然而,在 TS 中对于 window.MyNamespace = window.MyNamespace || {};
这行代码,TS 编译器会提示以下异常信息:
Property 'MyNamespace' does not exist on type 'Window & typeof globalThis'.(2339)
复制代码
以上异常信息是说在 Window & typeof globalThis
交叉类型上不存在 MyNamespace
属性。那么如何解决这个问题呢?最简单的方式就是使用类型断言:
(window as any).MyNamespace = {};
复制代码
虽然使用 any
大法可以解决上述问题,但更好的方式是扩展 lib.dom.d.ts
文件中的 Window
接口来解决上述问题,具体方式如下:
declare interface Window {
MyNamespace: any;
}
window.MyNamespace = window.MyNamespace || {};
复制代码
下面我们再来看一下 lib.dom.d.ts
文件中声明的 Window
接口:
/**
* A window containing a DOM document; the document property
* points to the DOM document loaded in that window.
*/
interface Window extends EventTarget, AnimationFrameProvider, GlobalEventHandlers,
WindowEventHandlers, WindowLocalStorage, WindowOrWorkerGlobalScope, WindowSessionStorage {
// 已省略大部分内容
readonly devicePixelRatio: number;
readonly document: Document;
readonly top: Window;
readonly window: Window & typeof globalThis;
addEventListener(type: string, listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof WindowEventMap>(type: K,
listener: (this: Window, ev: WindowEventMap[K]) => any,
options?: boolean | EventListenerOptions): void;
[index: number]: Window;
}
复制代码
在上面我们声明了两个相同名称的 Window
接口,这时并不会造成冲突。TypeScript
会自动进行接口合并,即把双方的成员放到一个同名的接口中。
如何为对象动态分配属性
在 JavaScript
中,我们可以很容易地为对象动态分配属性,比如:
let developer = {};
developer.name = "semlinker";
复制代码
以上代码在 JavaScript
中可以正常运行,但在 TypeScript
中,编译器会提示以下异常信息:
Property 'name' does not exist on type '{}'.(2339)
复制代码
{}
类型表示一个没有包含成员的对象,所以该类型没有包含 name
属性。为了解决这个问题,我们可以声明一个 LooseObject
类型:
interface LooseObject {
[key: string]: any
}
复制代码
该类型使用 索引签名 的形式描述 LooseObject
类型可以接受 key
类型是字符串,值的类型是 any
类型的字段。有了 LooseObject
类型之后,我们就可以通过以下方式来解决上述问题:
interface LooseObject {
[key: string]: any
}
let developer: LooseObject = {};
developer.name = "semlinker";
复制代码
对于 LooseObject
类型来说,它的约束是很宽松的。在一些应用场景中,我们除了希望能支持动态的属性之外,也希望能够声明一些必选和可选的属性。
比如对于一个表示开发者的 Developer
接口来说,我们希望它的 name
属性是必填,而 age
属性是可选的,此外还支持动态地设置字符串类型的属性。针对这个需求我们可以这样做:
interface Developer {
name: string;
age?: number;
[key: string]: any
}
let developer: Developer = { name: "semlinker" };
developer.age = 30;
developer.city = "XiaMen";
复制代码
其实除了使用 索引签名 之外,我们也可以使用 TypeScript
内置的工具类型 Record
来定义 Developer
接口:
// type Record<K extends string | number | symbol, T> = { [P in K]: T; }
interface Developer extends Record<string, any> {
name: string;
age?: number;
}
let developer: Developer = { name: "semlinker" };
developer.age = 30;
developer.city = "XiaMen";
复制代码
如何理解泛型中的 <T>
对于刚接触 TypeScript
泛型的读者来说,首次看到<T>
语法会感到陌生。其实它没有什么特别,就像传递参数一样,我们传递了我们想要用于特定函数调用的类型。
参考上面的图片,当我们调用 identity<Number>(1)
,Number
类型就像参数 1 一样,它将在出现 T 的任何位置填充该类型。图中 <T>
内部的 T
被称为类型变量,它是我们希望传递给 identity
函数的类型占位符,同时它被分配给 value
参数用来代替它的类型:此时 T
充当的是类型,而不是特定的 Number
类型。
其中 T
代表 Type
,在定义泛型时通常用作第一个类型变量名称。但实际上 T
可以用任何有效名称代替。除了 T
之外,以下是常见泛型变量代表的意思:
- K(Key):表示对象中的键类型;
- V(Value):表示对象中的值类型;
- E(Element):表示元素类型。
其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量 U,用于扩展我们定义的 identity 函数:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity<Number, string>(68, "Semlinker"));
复制代码
除了为类型变量显式设定值之外,一种更常见的做法是使编译器自动选择这些类型,从而使代码更简洁。我们可以完全省略尖括号,比如:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity(68, "Semlinker"));
复制代码
对于上述代码,编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T
和 U
,而不需要开发人员显式指定它们。
如何理解装饰器的作用
在 TypeScript
中装饰器分为类装饰器、属性装饰器、方法装饰器和参数装饰器四大类。装饰器的本质是一个函数,通过装饰器我们可以方便地定义与对象相关的元数据。
比如在 ionic-native
项目中,它使用 Plugin
装饰器来定义 IonicNative
中 Device
插件的相关信息:
@Plugin({
pluginName: 'Device',
plugin: 'cordova-plugin-device',
pluginRef: 'device',
repo: 'https://github.com/apache/cordova-plugin-device',
platforms: ['Android', 'Browser', 'iOS', 'macOS', 'Windows'],
})
@Injectable()
export class Device extends IonicNativePlugin {}
复制代码
在以上代码中 Plugin
函数被称为装饰器工厂,调用该函数之后会返回类装饰器,用于装饰 Device
类。Plugin
工厂函数的定义如下:
// https://github.com/ionic-team/ionic-native/blob/v3.x/src/%40ionic-native/core/decorators.ts
export function Plugin(config: PluginConfig): ClassDecorator {
return function(cls: any) {
// 把config对象中属性,作为静态属性添加到cls类上
for (let prop in config) {
cls[prop] = config[prop];
}
cls['installed'] = function(printWarning?: boolean) {
return !!getPlugin(config.pluginRef);
};
// 省略其他内容
return cls;
};
}
复制代码
通过观察 Plugin
工厂函数的方法签名,我们可以知道调用该函数之后会返回 ClassDecorator
类型的对象,其中 ClassDecorator
类型的声明如下所示:
declare type ClassDecorator = <TFunction extends Function>(target: TFunction)
=> TFunction | void;
复制代码
类装饰器顾名思义,就是用来装饰类的。它接收一个参数 —— target
: TFunction
,表示被装饰器的类。介绍完上述内容之后,我们来看另一个问题 @Plugin({...})
中的 @
符号有什么用?
其实 @Plugin({...})
中的 @
符号只是语法糖,为什么说是语法糖呢?这里我们来看一下编译生成的 ES5
代码:
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var Device = /** @class */ (function (_super) {
__extends(Device, _super);
function Device() {
return _super !== null && _super.apply(this, arguments) || this;
}
Device = __decorate([
Plugin({
pluginName: 'Device',
plugin: 'cordova-plugin-device',
pluginRef: 'device',
repo: 'https://github.com/apache/cordova-plugin-device',
platforms: ['Android', 'Browser', 'iOS', 'macOS', 'Windows'],
}),
Injectable()
], Device);
return Device;
}(IonicNativePlugin));
复制代码
通过生成的代码可知,@Plugin({...})
和 @Injectable()
最终会被转换成普通的方法调用,它们的调用结果最终会以数组的形式作为参数传递给 __decorate
函数,而在 __decorate
函数内部会以 Device
类作为参数调用各自的类型装饰器,从而扩展对应的功能。
此外,如果你有使用过 Angular
,相信你对以下代码并不会陌生。
const API_URL = new InjectionToken('apiUrl');
@Injectable()
export class HttpService {
constructor(
private httpClient: HttpClient,
@Inject(API_URL) private apiUrl: string
) {}
}
复制代码
在 Injectable
类装饰器修饰的 HttpService
类中,我们通过构造注入的方式注入了用于处理 HTTP
请求的 HttpClient
依赖对象。而通过 Inject
参数装饰器注入了 API_URL
对应的对象,这种方式我们称之为依赖注入(Dependency Injection)
。
如何理解函数重载的作用
可爱又可恨的联合类型
由于 JavaScript
是一个动态语言,我们通常会使用不同类型的参数来调用同一个函数,该函数会根据不同的参数而返回不同的类型的调用结果:
function add(x, y) {
return x + y;
}
add(1, 2); // 3
add("1", "2"); //"12"
复制代码
由于 TypeScript
是 JavaScript
的超集,因此以上的代码可以直接在 TypeScript
中使用,但当 TypeScript
编译器开启 noImplicitAny
的配置项时,以上代码会提示以下错误信息:
Parameter 'x' implicitly has an 'any' type.
Parameter 'y' implicitly has an 'any' type.
复制代码
该信息告诉我们参数 x
和参数 y
隐式具有 any
类型。为了解决这个问题,我们可以为参数设置一个类型。因为我们希望 add 函数同时支持 string
和 number
类型,因此我们可以定义一个 string | number
联合类型,同时我们为该联合类型取个别名:
type Combinable = string | number;
复制代码
在定义完 Combinable
联合类型后,我们来更新一下 add
函数:
function add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
复制代码
为 add
函数的参数显式设置类型之后,之前错误的提示消息就消失了。那么此时的 add
函数就完美了么,我们来实际测试一下:
const result = add('semlinker', ' kakuqo');
result.split(' ');
复制代码
在上面代码中,我们分别使用 'semlinker'
和 'kakuqo'
这两个字符串作为参数调用 add
函数,并把调用结果保存到一个名为 result
的变量上,这时候我们想当然的认为此时 result
的变量的类型为 string
,所以我们就可以正常调用字符串对象上的 split
方法。但这时 TypeScript
编译器又出现以下错误信息了:
Property 'split' does not exist on type 'Combinable'.
Property 'split' does not exist on type 'number'.
复制代码
很明显 Combinable
和 number
类型的对象上并不存在 split
属性。问题又来了,那如何解决呢?这时我们就可以利用 TypeScript
提供的函数重载。
函数重载
函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
// type Combinable = string | number;
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
复制代码
在以上代码中,我们为 add
函数提供了多个函数类型定义,从而实现函数的重载。在 TypeScript
中除了可以重载普通函数之外,我们还可以重载类中的成员方法。
方法重载是指在同一个类中方法同名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同),调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术。所以类中成员方法满足重载的条件是:在同一个类中,方法名相同且参数列表不同。下面我们来举一个成员方法重载的例子:
class Calculator {
add(a: number, b: number): number;
add(a: string, b: string): string;
add(a: string, b: number): string;
add(a: number, b: string): string;
add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
}
const calculator = new Calculator();
const result = calculator.add('Semlinker', ' Kakuqo');
复制代码
这里需要注意的是,当 TypeScript
编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。如果匹配的话就使用这个。因此,在定义重载的时候,一定要把最精确的定义放在最前面。另外在 Calculator
类中,add(a: Combinable, b: Combinable){ }
并不是重载列表的一部分,因此对于 add
成员方法来说,我们只定义了四个重载方法。
interfaces 与 type 之间有什么区别
- Objects/Functions
接口和类型别名都可以用来描述对象的形状或函数签名:
接口
interface Point {
x: number;
y: number;
}
interface SetPoint {
(x: number, y: number): void;
}
复制代码
类型别名
type Point = {
x: number;
y: number;
};
type SetPoint = (x: number, y: number) => void;
复制代码
- Other Types
与接口类型不一样,类型别名可以用于一些其他类型,比如原始类型、联合类型和元组:
// primitive
type Name = string;
// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };
// union
type PartialPoint = PartialPointX | PartialPointY;
// tuple
type Data = [number, string];
复制代码
- Extend
接口和类型别名都能够被扩展,但语法有所不同。此外,接口和类型别名不是互斥的。接口可以扩展类型别名,而反过来是不行的。
Interface extends interface
interface PartialPointX { x: number; }
interface Point extends PartialPointX {
y: number;
}
Type alias extends type alias
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };
Interface extends type alias
type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }
Type alias extends interface
interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };
复制代码
- Implements
类可以以相同的方式实现接口或类型别名,但类不能实现使用类型别名定义的联合类型:
interface Point {
x: number;
y: number;
}
class SomePoint implements Point {
x = 1;
y = 2;
}
type Point2 = {
x: number;
y: number;
};
class SomePoint2 implements Point2 {
x = 1;
y = 2;
}
type PartialPoint = { x: number; } | { y: number; };
// A class can only implement an object type or
// intersection of object types with statically known members.
class SomePartialPoint implements PartialPoint { // Error
x = 1;
y = 2;
}
复制代码
- Declaration merging
与类型别名不同,接口可以定义多次,会被自动合并为单个接口。
interface Point { x: number; }
interface Point { y: number; }
const point: Point = { x: 1, y: 2 };
复制代码