TypeScript 学习笔记
简介
在 TypeScript 的学习过程中,我发现这个语言有点抽象,团队成员那边已经给出了基础部分的学习笔记,我这边就记录一下学习过程中遇到的难点。
接口的妙用
混合类型
接口能够描述 JavaScript 里丰富的类型。 因为 JavaScript 其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。
一个例子就是,一个对象可以同时做为函数和对象使用,并带有额外的属性。
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = <Counter>function (start: number) { };
counter.interval = 123;
counter.reset = function () { };
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
type ApiResponse<T> =
| {status: 'success'; data: T; timestamp: Date }
| {status: 'error'; message: string; timestamp: Date };
let response1: ApiResponse<number> = {
status: 'success',
data: 100,
timestamp: new Date()
};
let response2: ApiResponse<number> = {
status: 'error',
message: 'There was an error',
timestamp: new Date()
};
类的一些特性
存取器
首先,我们从一个没有使用存取器的例子开始。
class Employee {
fullName: string;
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
console.log(employee.fullName);
}
我们可以随意的设置 fullName ,这是非常方便的,但是这也可能会带来麻烦。
下面这个版本里,我们先检查用户密码是否正确,然后再允许其修改员工信息。 我们把对 fullName 的直接访问改成了可以检查密码的 set 方法。 我们也加了一个 get 方法,让上面的例子仍然可以工作。
let passcode = "secret passcode";
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (passcode && passcode == "secret passcode") {
this._fullName = newName;
}
else {
console.log("Error: Unauthorized update of employee!");
}
}
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
alert(employee.fullName);
}
我们可以修改一下密码,来验证一下存取器是否是工作的。当密码不对时,会提示我们没有权限去修改员工。
只带有get不带有set的存取器自动被推断为readonly。 这在从代码生成.d.ts文件时是有帮助的,因为利用这个属性的用户会看到不允许够改变它的值。
( TypeScript 简直是语法大杂烩, get 和 set 关键字都来了
函数
剩余参数
必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在JavaScript里,你可以使用arguments来访问所有传入的参数。
在TypeScript里,你可以把所有参数收集到一个变量里:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
this
这个好复杂,可以看文档
枚举
怪异的枚举值
enum FileAccess {
// constant members
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// computed member
G = "123".length
}
断言
尖括号语法: 尖括号语法是 TypeScript 中最早引入的断言形式。它使用 <Type> ,其中 Type 是您要将值断言为的目标类型。
let someValue: any = "Hello, World!"; let strLength: number = (<string>someValue).length;
as 关键字: "as" 关键字是更现代和推荐的断言语法,它在 JSX 和 TSX 中也更有用。它使用value as Type形式。
let someValue: any = "Hello, World!"; let strLength: number = (someValue as string).length;
高级类型
交叉类型(Intersection Types)
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如,Person & Serializable & Loggable 同时是 Person 和 Serializable 和 Loggable 。 就是说这个类型的对象同时拥有了这三种类型的成员。
类型别名
类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === 'string') {
return n;
}
else {
return n();
}
}
同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:
type Container<T> = { value: T };
与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型。
type LinkedList<T> = T & { next: LinkedList<T> };
interface Person {
name: string;
}
var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;
#索引类型(Index types)(十分抽象的家伙
使用索引类型,编译器就能够检查使用了动态属性名的代码。 例如,一个常见的JavaScript模式是从对象中选取属性的子集。
function pluck(o, names) {
return names.map(n => o[n]);
}
下面是如何在 TypeScript 里使用此函数,通过索引类型查询和索引访问操作符:
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
return names.map(n => o[n]);
}
interface Person {
name: string;
age: number;
}
let person: Person = {
name: 'Jarid',
age: 35
};
let strings: string[] = pluck(person, ['name']); // ok, string[]
编译器会检查 name 是否真的是 Person 的一个属性。 本例还引入了几个新的类型操作符。 首先是 keyof T ,索引类型查询操作符。 对于任何类型 T,keyof T 的结果为 T 上已知的公共属性名的联合。 例如:
let personProps: keyof Person; // 'name' | 'age'
keyof Person 是完全可以与'name' | 'age'互相替换的。 不同的是如果你添加了其它的属性到 Person ,例如 address : string,那么 keyof Person 会自动变为 'name' | 'age' | 'address' 。 你可以在像 pluck 函数这类上下文里使用 keyof ,因为在使用之前你并不清楚可能出现的属性名。 但编译器会检查你是否传入了正确的属性名给 pluck :
pluck(person, ['age', 'unknown']); // error, 'unknown' is not in 'name' | 'age'
第二个操作符是 T[K] ,索引访问操作符。 在这里,类型语法反映了表达式语法。 这意味着 person['name'] 具有类型Person['name'] — 在我们的例子里则为string类型。 然而,就像索引类型查询一样,你可以在普通的上下文里使用T[K],这正是它的强大所在。 你只要确保类型变量 K extends keyof T 就可以了。 例如下面 getProperty 函数的例子:
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
return o[name]; // o[name] is of type T[K]
}
getProperty 里的 o: T和name: K ,意味着 o[name]: T[K] 。 当你返回 T[K] 的结果,编译器会实例化键的真实类型,因此 getProperty 的返回值类型会随着你需要的属性改变。
let name: string = getProperty(person, 'name');
let age: number = getProperty(person, 'age');
let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'
#映射类型
一个常见的任务是将一个已知的类型每个属性都变为可选的:
interface PersonPartial {
name?: string;
age?: number;
}
或者我们想要一个只读版本:
interface PersonReadonly {
readonly name: string;
readonly age: number;
}
这在 JavaScript 里经常出现,TypeScript 提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。 例如,你可以令每个属性成为 readonly 类型或可选的。 下面是一些例子:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
type Partial<T> = {
[P in keyof T]?: T[P];
}
像下面这样使用:
type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;
#由映射类型进行推断
现在你了解了如何包装一个类型的属性,那么接下来就是如何拆包。 其实这也非常容易:
function unproxify<T>(t: Proxify<T>): T {
let result = {} as T;
for (const k in t) {
result[k] = t[k].get();
}
return result;
}
let originalProps = unproxify(proxyProps);
注意这个拆包推断只适用于同态的映射类型。 如果映射类型不是同态的,那么需要给拆包函数一个明确的类型参数。
命名空间和模块
使用命名空间
命名空间是位于全局命名空间下的一个普通的带有名字的 JavaScript 对象。 这令命名空间十分容易使用。 它们可以在多文件中同时使用,并通过 --outFile 结合在一起。 命名空间是帮你组织 Web 应用不错的方式,你可以把所有依赖都放在 HTML 页面的 <script> 标签里。
但就像其它的全局命名空间污染一样,它很难去识别组件之间的依赖关系,尤其是在大型的应用中。
使用模块
像命名空间一样,模块可以包含代码和声明。 不同的是模块可以声明它的依赖。
模块会把依赖添加到模块加载器上(例如 CommonJs / Require.js )。 对于小型的 JS 应用来说可能没必要,但是对于大型应用,这一点点的花费会带来长久的模块化和可维护性上的便利。 模块也提供了更好的代码重用,更强的封闭性以及更好的使用工具进行优化。
对于 Node.js 应用来说,模块是默认并推荐的组织代码的方式。
从 ECMAScript 2015 开始,模块成为了语言内置的部分,应该会被所有正常的解释引擎所支持。 因此,对于新项目来说推荐使用模块做为组织代码的方式。