Blogs


  • 首页

  • 归档

  • 标签

7.枚举

发表于 2017-11-20 | 分类于 翻译 , 编程语言 , TypeScript

Enums

枚举允许我们定义一系列命名变量。TypeScript提拱基于数值和基于串的枚举。

数值枚举

我们从数值枚举开始。使用enum关键字来定义枚举。

1
2
3
4
5
6
7
enum Direction {
Up = 1,
Down,
Left,
Right,
}

这里,我们定义了一个数值枚举,并将Up的值初始化为1。后面的所有成员的值依次增1。即Down为2,Left为3,Right为4。

也可以完全不要初始化:

1
2
3
4
5
6
enum Direction {
Up,
Down,
Left,
Right,
}

这里,Up的值为0,Down值为1,依次类推。这种枚举值自增的机制在我们不在意值本身是多少而只在意值是不同的情况下是很有用的。

枚举的使用是很简单的:就像访问enum自己的属性一样来使用枚举的成员, 使用枚举的名字来声明某变量是某枚举类型。

1
2
3
4
5
6
7
8
9
10
enum Response {
No = 0,
Yes = 1,
}
function respond(recipient: string, message: Response): void {
// ...
}
respond("Princess Caroline", Response.Yes)

数值枚举可混用计算值和常量值(来初始化)。简单的说,没有初始化的枚举值要么定义在前面,要么定义在由数值常量初始化后的枚举值后。换句话说,下面这样声明枚举是不行的:

1
2
3
4
enum E {
A = getSomeValue(),
B, // error! 'A' is not constant-initialized, so 'B' needs an initializer
}

串枚举

字符串枚举是一个类似的概念,但在运行时有点微妙的不同,如下所述。在字符串枚举中,每个成员必须要用串字面量或另一个串枚举来初始化

1
2
3
4
5
6
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}

尽管串枚举没有自增机制,但串枚举值有意义明确的优势。换句话说,如果你在调试代码,你就不得不读那些意义不明的数值枚举,这些值不能传递出有意义的信息(通过反向映射可能会有用),而使用串枚举就会给你一个清晰的、可读的值,和枚举成员的名字无关。

Heterogeneous enums

从技术上讲,枚举可混用串和数值值。但除非你真的想以一种聪明的方式利用JavaScript运行时,否则别这样:

1
2
3
4
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}

计算值成员和常量成员

每个枚举成员有一个相关的值,这个值可是计算值(computed)和常量。枚举成员有下列情况的,该枚举成员是常量:

  • 是第一个成员并且没有初始化,这时其值是0:

    1
    2
    // E.X is constant:
    enum E { X }
  • 没有初始化,并前一个枚举成员是数值储量。这时,该枚举成员的值是前一个枚举成员的值加一。

    1
    2
    3
    4
    5
    6
    7
    // All enum members in 'E1' and 'E2' are constant.
    enum E1 { X, Y, Z }
    enum E2 {
    A = 1, B, C
    }
  • 使用常量枚举表达式初始化的成员。常量枚举表达式是TypeScript表达式的一个子集,可在编译时被求解。下面都是枚举常量表达式:
    1. 字面量枚举表达式(串字面量或数值字面量)
    2. 对前面定义的常量枚举成员的引用
    3. 应用一元运算符+,-,~于枚举表达式
    4. 二元运算符+,-,*,/,%,<<,>>,>>>,&,|,^ 应用与两个常量枚举

如果表达式的值计算出来是NaN或Infinity将得到一个编译是错误。

其它的枚举即为计算值

1
2
3
4
5
6
7
8
9
enum FileAccess {
// constant members
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// computed member
G = "123".length
}

Union enums and enum member types

一个字面量枚举成员是一个没初始化的常枚举成员

any string literal (e.g. “foo”, “bar, “baz”)
any numeric literal (e.g. 1, 100)
a unary minus applied to any numeric literal (e.g. -1, -100)
When all members in an enum have literal enum values, some special semantics come to play.

The first is that enum members also become types as well! For example, we can say that certain members can only have the value of an enum member:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum ShapeKind {
Circle,
Square,
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
let c: Circle = {
kind: ShapeKind.Square,
// ~~~~~~~~~~~~~~~~ Error!
radius: number,
}

The other change is that enum types themselves effectively become a union of each enum member. While we haven’t discussed union types yet, all that you need to know is that with union enums, the type system is able to leverage the fact that it knows the exact set of values that exist in the enum itself. Because of that, TypeScript can catch silly bugs where we might be comparing values incorrectly. For example:

enum E {
Foo,
Bar,
}

function f(x: E) {
if (x !== E.Foo || x !== E.Bar) {
// ~~~
// Error! Operator ‘!==’ cannot be applied to types ‘E.Foo’ and ‘E.Bar’.
}
}
In that example, we first checked whether x was not E.Foo. If that check succeeds, then our || will short-circuit, and the body of the ‘if’ will get run. However, if the check didn’t succeed, then x can only be E.Foo, so it doesn’t make sense to see whether it’s equal to E.Bar.

运行时的枚举

枚举在运行时以对象的方式存在,例如:

1
2
3
enum E {
X, Y, Z
}

这个枚举E可作为参数传递给下面的函数。

1
2
3
4
5
6
function f(obj: { X: number }) {
return obj.X;
}
// Works, since 'E' has a property named 'X' which is a number.
f(E);

逆向映射

除了为枚举创建了一个带属性的对象,数值属性也有一个从值到枚举名字的逆向映射。例如,下面的例子:

1
2
3
4
5
enum Enum {
A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"

TypeScript可能会把这段代码编译成下面的JavaScript:

1
2
3
4
5
6
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
var a = Enum.A;
var nameOfA = Enum[a]; // "A"

在这段产生的代码中,一个枚举被编译成了一个对象,该对象保存了两个方向的映射(name -> value) 和 (value -> name)。对枚举成员的访问总是被编译成对属性的访问而不是将其值内联。

记住,串枚举没有逆向映射。

常枚举 const enums

大多数情况下,枚举是一个很好的解决方案,然而有时候条件更为苛刻。为了避免编译后代码的额外开销,则可以使用常枚举。常枚举使用const修饰符来定义。

1
2
3
4
const enum Enum {
A = 1,
B = A * 2
}

常枚举只能有常枚举表达式(作其成员),和普通枚举不同的是常枚举会在编译时被内联到使用的地方。这只有在常枚举没有计算值的才是可能的。

1
2
3
4
5
6
7
8
const enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]

产生的代码为:

1
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];

Ambient enums

Ambient 枚举用来描述已经存在的枚举类型

1
2
3
4
5
declare enum Enum {
A = 1,
B,
C = 2
}

ambient枚举和非ambient枚举的一个重要的不同是那些没有初始化的成员,如果其前一个成员是常量值成员,它也将被看作常量成员。相反,一个没有初始化的ambient枚举成员将被看作是计算值成员。

6.泛型

发表于 2017-11-01 | 分类于 翻译 , 编程语言 , TypeScript

简介

软件工程活动中的一个主要方面是构建组件,这些组件不仅仅是有定义好的API,也要是可复用的。那些对于今天的数据是可用的,对于将来的数据也是可用的组件,将在构建大型软件系统时给你最大的灵活性。
在C#或Java这样的语言中,一个主要的功能就是用泛型来创建可复用的组件。泛型表示这些组件可用在许多不同的数据类型上,尔不是单一的数据类型。这就允许用户在这些组件里面使用自己的数据类型。

Hello World

让我们写一个泛型版的Hello World: ID 函数。ID函数指的是一种你给它什么它就返回给你什么的函数,就像echo命令一样。

1
2
3
function identity(arg: number): number {
return arg;
}

或者,我们可以使用any类型:

1
2
3
function identity(arg: any): any {
return arg;
}

这里实际上该用泛型,但是用了any,这会导致函数返回any 类型而丢失掉了类型信息。例如,传入了一个number类型,对于返回的类型却只知道它是any(注:从而失去了类型检查和进一步的类型推导)。

所以,我们需要一种捕获类型的方法,然后我们可用这种方法了标示返回类型。在这里,我们使用类型变量——一种特殊的变量为类型而生而不是为值而生。

1
2
3
function identity<T>(arg:T){
return arg
}

现在我们给Id函数加入了类型变量T。这个T让我们可以捕获用户提供的类型(例如number),所以我们就可以使用这个类型。在这里,我们把T用做返回值类型。

我们说这个版本的id函数是个泛型函数,应为它可以用在很多类型上。和用any不同,它和number类型的哪个id函数一样是精准的(如,不会丢失类型信息)。

一旦我们写好了泛型函数,我们有两种调用它的方式。第一种是传递所有的参数给它,包括类型参数:

1
let output = identity<string>("myOutput")

这里,我们明确的指定了T为string,其作为调用函数的一个类型参数,需用用尖括号括起来而不是圆括号。
第二种方式更为通用,即使用类型推断,即我们让编译器根据我们传入的类型自行设置T的值。

1
let output = identity("myString")

注意,我们没有用尖括号来明确指明类型参数,编译器根据"myString"来决定T的值。尽管类型参数的自动推断可以使代码更短,可读性更强,但是有些复杂的情况下编译器不能推断出泛型的类型,这时就需要你明确指定泛型的类型。

使用泛型类型变量

当你使用泛型时你会注意到当你创建像identity这样的泛型函数的时候,编译器会确保你在函数体内正确的使用了泛型。即,你实际应该把看做是所有类型。
如果我们想在函数体内把arg的length输出出来,会发生什么呢? 我们可能会这样写:

1
2
3
4
function loggingIdentity<T>(arg:T):T{
console.log(arg.length);
return arg;
}

如果我们这样做,编译器会报告一个错误,即我们使用了arg的length成员,但是我们没说过arg有这样一个成员。上面说过,你应该把T看成是所有类型,所以可能传进来的arg是一个数字,那么它是没有length属性的。

这里我们实际上是希望这个函数接受T数组而不是T。如果arg是T的数组,那么就可以访问.length属性了。

1
2
3
4
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}

现在你可以把这个函数解读为:一个拥有类型参数T以及函数参数arg—— 为数组T类型 ——并返回T数组类型的泛型函数。如果我们传入一个数字数组,我们会得到一个返回的数字数组,那么T为number。
这让我们可以把泛型类型T作为所有我们用到的类型的一个部分,而不是所有类型。这样给我们更多的灵活性。
对上面的例子我们也可使用下面的形式:

1
2
3
4
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}

你也许很熟悉这种写法,在下节中,我们将会讨论如何创建类似于Array<T>这样的泛型。

泛形类型

在前面几个小节中我们创建了一个泛型函数,它可又接受一些类型,在这节中我们将探索函数自身的类型以及如何创建泛型接口。
泛型函数的类型和非泛型函数的类型是类似的,需把泛型参数写在最前面。

1
2
3
4
5
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <T>(arg: T) => T = identity;

在类型中,我们也可以使用不同的泛型参数名,只要泛型参数的数量和用的位置是对的就可以。

1
2
3
4
5
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <U>(arg: U) => U = identity;

我们也可用对象字面量的方式来表示泛型函数得类型:

1
2
3
4
5
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: {<T>(arg: T): T} = identity;

我们可以把这个例子写成一个接口。

1
2
3
4
5
6
7
8
9
interface GenericIdentityFn {
<T>(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = identity;

或者,我们可以把泛型参数做为整个接口得泛型参数。这样,该接口的所有成员都可以使用该泛型参数。

1
2
3
4
5
6
7
8
9
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;

注意,我们的例子变的有点不同了。
<!–
. Instead of describing a generic function, we now have a non-generic function signature that is a part of a generic type. When we use GenericIdentityFn, we now will also need to specify the corresponding type argument (here: number), effectively locking in what the underlying call signature will use. Understanding when to put the type parameter directly on the call signature and when to put it on the interface itself will be helpful in describing what aspects of a type are generic.

In addition to generic interfaces, we can also create generic classes. Note that it is not possible to create generic enums and namespaces.
–>

泛型类

泛型类和浮现接口有类似的结构。泛型类在类名后面有一个泛型参数表。

1
2
3
4
5
6
7
8
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

1
2
3
4
5
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };
alert(stringNumeric.add(stringNumeric.zeroValue, "test"));

就像使用接口一样,在类上使用泛型参数可让类中的的属性或方法可以使用这个泛型类型。

在类这章描述过,类的类型有两种方面:类方面和实例方面。泛型类的泛型只在其实例中可用。也就是说,静态方法不能访问泛型类的浮现参数。

对泛型的约束

如果你还记得前面的例子,你也许希望你的泛型函数中的泛型类型只实用于一部分的类型。对于loggingIdentity这个例子中,我们希望能访问arg的length属性,但是编译器无法保证每个类型都有一个length属性,所以它会给出一个警告。

1
2
3
4
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}

除了使用any类型或所有类型,我们希望这个函数使用那些带有.length属性的类型。只要某类型有.length属性,我们就允许它被传递到函数中,而且只是要求这个属性是必须要有的。为了做到这点,我们必须对T作一些限定。

为了对T作限定,我们使用接口来描述我们的限定。这里我们创建一个拥有单一属性length的接口,并使用这个接口和extends关键字来说明我们的限制。

1
2
3
4
5
6
7
8
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}

因为现在该泛型函数被限制了泛型的类型,那么不再可用任意类型作这个泛型了。

1
loggingIdentity(3); // Error, number doesn't have a .length property

除非我们传递一个有length属性的东西给它。

1
loggingIdentity({length: 10, value: 3});

在类型参数中使用限定

你可用一个类型参数来限定另一个类型参数。例如,我们想从一个给定的名字来访问一个对象的属性。我们希望能确保我们不会意外的访问了那些不存在的属性。所以我们在这两个泛型类型中应用一个限定。

1
2
3
4
5
6
7
8
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

在泛型中使用类类型

When creating factories in TypeScript using generics, it is necessary to refer to class types by their constructor functions. For example,
当我们在工厂函数中使用泛型的时候,在其构造函数中引用类类型是有必要的。例如:

1
2
3
function create<T>(c: {new(): T; }): T {
return new c();
}

下面的例子演示了使用原型来引用和限定构造函数和类实例之间的关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class BeeKeeper {
hasMask: boolean;
}
class ZooKeeper {
nametag: string;
}
class Animal {
numLegs: number;
}
class Bee extends Animal {
keeper: BeeKeeper;
}
class Lion extends Animal {
keeper: ZooKeeper;
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag; // typechecks!
createInstance(Bee).keeper.hasMask; // typechecks!

5.函数

发表于 2017-10-28 | 分类于 翻译 , 编程语言 , TypeScript

简介

函数是任何一个JavaScript程序中的基本构建单位。用它们你可以构建抽象层、模拟类、隐藏信息和模块化。在TypeScript中,尽管本来就有类、名字空间和模块这些东西,函数依然伴演了描述如何做某些事的关键角色。TypeScript也在标准的JavaScript函数上添加了一些东西来使它们更易于使用。

函数

和JavaScript一样,TypeScript可创建命名的函数和匿名的函数。这允许你选择最合适你的应用的方式,是在创建一个API的函数列表还是一个传递给另一个函数的一次性函数(a one-off function to hand off to another function).
下面的例子简要的演示了JavaScript中这两这方式:

1
2
3
4
5
6
7
// Named function
function add(x, y) {
return x + y;
}
// Anonymous function
let myAdd = function(x, y) { return x + y; };

就像在JavaScript中一样,函数可以引用函数体外部的变量,这叫做变量捕获。

1
2
3
4
5
let z = 100;
function addToZ(x, y) {
return x + y + z;
}

函数类型

添加类型

让我们给前面的例子加上函数类型:

1
2
3
4
5
function add(x: number, y: number): number {
return x + y;
}
let myAdd = function(x: number, y: number): number { return x + y; };

我们能为函数的每个参数添加类型、给函数自身添加类型以及给函数的返回值添加类型。TypeScript可以通过返回语句推断出返回值的类型,所以在大多数情况下,我们可以不用指定返回值的类型。

写函数类型

现在我们只定义了函数,让我们写出完整的函数类型:

1
2
let myAdd: (x: number, y: number) => number =
function(x: number, y: number): number { return x + y; };

一个函数类型有两个部分:参数的类型和返回值的类型。当要写出完整的函数的类型,这两个部分都是必要的。我们像写函数的参数列表那样写出参数的类型,给每个参数一个名字和一个类型。这个名字只为了增加代码的可读性,我们可写成

1
2
let myAdd: (baseValue: number, increment: number) => number =
function(x: number, y: number): number { return x + y; };

只要参数的类型和函数参数的类型是一致的,不管名字是否一致都是可以的。
第二部分是返回值类型。我们使用了一个胖箭头(=>)来表式其后是返回值的类型。如前面所说,这是函数类型中必不可少的一部分,所以如果一个函数不返回值你需要使用void来表示。
注意,函数类型仅由参数类型和返回值类型构成,而不包括捕获的变量的类型。结果是,捕获的变量作为了函数的隐性状态。

推断类型

你也许注意到了,如果你省略了等号一边的类型,TypeScript的编译器可以猜测出类型。

1
2
3
4
5
6
// myAdd has the full function type
let myAdd = function(x: number, y: number): number { return x + y; };
// The parameters 'x' and 'y' have the type number
let myAdd: (baseValue: number, increment: number) => number =
function(x, y) { return x + y; };

这叫作上下文类型,类型推断的一种形似。这可以减少和类型相关的工作量。

可选和默认参数

TypeScript会假设每个参数都是必须的。在并不意味着不能传递null或undefined给它,而是在每个函数被调用的时候,编译器会检察用户是否为每个参数提拱了值。编译器也会假设这些参数就刚好是要传递到函数中的参数(注:数量假设)。简而言之,给出的参数的个数要和函数期望的参数的个数相匹配。

1
2
3
4
5
6
7
function buildName(firstName: string, lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // ah, just right

在JavaScript中,每个参数都是可选的,用户可能会省略其中的一些参数,如果参数被省略了那么该参数就是undefined。在TypeScript中可以通过在参数名末尾添加?来使用这一功能。比如,如果第二个参数是可选的:

1
2
3
4
5
6
7
8
9
10
function buildName(firstName: string, lastName?: string) {
if (lastName)
return firstName + " " + lastName;
else
return firstName;
}
let result1 = buildName("Bob"); // works correctly now
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // ah, just right

可选参数必须要在必选参数后面声明。
在TypeScript中,我们也可以参数设置一个预设值,如果调用者没有提拱该参数(或提拱了一个undefined),那么该参数的值就是预设值。这被叫做默认参数.

1
2
3
4
5
6
7
8
function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // works correctly now, returns "Bob Smith"
let result2 = buildName("Bob", undefined); // still works, also returns "Bob Smith"
let result3 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result4 = buildName("Bob", "Adams"); // ah, just right

默认参数被视作是可选参数,和可选参数一样,我们可以在调用函数的时候忽略它们。可选参数和默认参数的类型(注:指的是函数的类型)是共用的。

1
2
3
function buildName(firstName: string, lastName?: string) {
// ...
}

和

1
2
3
function buildName(firstName: string, lastName = "Smith") {
// ...
}

的类型都是(firstName:string,lastName?:string)=>string. lastName的默认在在类中是没有体现的,只留下该参数是可选的这一事实。
和原生可选参数不同,默认参数不需要声明在必须参数后面。 。如果一个默认参数只必选参数之前,那我们需要明确的传递一个undefined给它。

1
2
3
4
5
6
7
8
function buildName(firstName = "Will", lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // okay and returns "Bob Adams"
let result4 = buildName(undefined, "Adams"); // okay and returns "Will Adams"

Rest 参数

必选,可选,默认参数有一个共同点:它们谈及的都是一个参数的情况。有时候你可能会想把多个参数”打包起来”,或者你压根就不知道函数实际上会接受到多少个参数。在JavaScript中,你可以使用arguments(注:JavaScript的魔术变量)来处理这种情况。
在TypeScript中,你可以把所有的参数收集到同一个变量中:

1
2
3
4
5
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

(注:ES6也有这种语法)
Rest参数被视为一组可选参数,你可以传递任意多的参数都Rest参数。编译器会创建一个参数数组,其名字在...后指定。

1
2
3
4
5
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;

this

学习如何使用this是学习JavaScript中的一条必经之路。TypeScript是JavaScript的超集,所以TypeScript的程序员也需要学习如何使用this以及指出什么时候this被错误的使用了。
幸运的是,TypeScript使用了一些技术来捕获this不正确的使用。如果你需要学习在JavaScript中如何使用this,请先阅读《理解JavaScript函数和this》,该文介绍了this内部的东西,我们这里只介绍一些基础。

this 和箭头函数

在JavaScript中,当函数被调用的时候其中的this是可用的。这是一个强大的、灵活的特性,但其代价是总是需要知道函数正在执行的上下文是什么。这是十分具有迷惑性的,特别是在返回一个函数或以一个函数为参数的时候。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
return function() {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);

注意,createCardPicker将返回一个函数。如果我们运行这个例子,我们会得到一个错误,而不是期望中的警告框。这是因为,createCardPicker返回的函数中的this是window而非deck对象。这是因为我们以顶层非方法的语法的方式在调用cardPicker(原注:在严格模式下,this会是undefined)。
我们可以通过确保function 绑定到正确的this的方式修正这个问题。这种情况下,无论这个函数多晚被调用,它总可以访问到deck对象。为了做到这点,我们可使用ES6的箭头函数,箭头函数会在函数被创建的时候捕获this。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
// NOTE: the line below is now an arrow function, allowing us to capture 'this' right here
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);

更进一步,如果你使用了--noImplicitThis选项,TypeScript会在你错误的使用this的时候给出一个警告。它会指出this.suits[pickedSuit]中的this是any类型的。

this 参数

不幸的是,this.suits[pickedSuit]的类型还是是any。这是因为this是来自于函数表达式中的对象字面量。你可以通过明确的指定一个this参数来修正这个问题。this参数是位于参数表中第一个参数的假参数。

1
2
3
function f(this: void) {
// make sure `this` is unusable in this standalone function
}

现在添加两个接口来使类型更加清晰和更易复用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
interface Card {
suit: string;
card: number;
}
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// NOTE: The function now explicitly specifies that its callee must be of type Deck
createCardPicker: function(this: Deck) {
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);

现在TypeScript知道createCardPicker需要在Deck对象上被调用,这表明this的Deck类型的而不是any,所以--noImplicitThis不会导致问题。

回调中的this参数。

当你传递一个函数到一个库中,你也会在回调中遇到this的问题。因为这些库会将你的回调作为一个普通函数来调用,this就会是undefined. 你可用this参数来防止一些错误。首先,库的作者需要这样表示回调:

1
2
3
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}

this: void means that addClickListener expects onclick to be a function that does not require a this type. Second, annotate your calling code with this:

class Handler {
info: string;
onClickBad(this: Handler, e: Event) {
// oops, used this here. using this callback would crash at runtime
this.info = e.message;
}
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // error!
With this annotated, you make it explicit that onClickBad must be called on an instance of Handler. Then TypeScript will detect that addClickListener requires a function that has this: void. To fix the error, change the type of this:

class Handler {
info: string;
onClickGood(this: void, e: Event) {
// can’t use this here because it’s of type void!
console.log(‘clicked!’);
}
}
let h = new Handler();
uiElement.addClickListener(h.onClickGood);
Because onClickGood specifies its this type as void, it is legal to pass to addClickListener. Of course, this also means that it can’t use this.info. If you want both then you’ll have to use an arrow function:

class Handler {
info: string;
onClickGood = (e: Event) => { this.info = e.message }
}
This works because arrow functions don’t capture this, so you can always pass them to something that expects this: void. The downside is that one arrow function is created per object of type Handler. Methods, on the other hand, are only created once and attached to Handler’s prototype. They are shared between all objects of type Handler.

重载

JavaScript是一种非常“动态”的语言。JavaScript中函数根据传入的参数返回不同的对象是很常见的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x): any {
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick the card
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);
let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

这里的pickCard根据传入的参数的不同会返回不同的东西。如果用户传递一个带表桌子的对象,这个函数会返回card,如果用户传入的是card,这个函数会告诉他是哪一个。那我们如何在类型系统中描述这种情况呢?

答案是为这个函数补充一些函数类型的重载。编译器用这些重载来解决函数调用时的问题。下面的例子演示了如何用重载来描述pickCard的参数和返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick the card
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);
let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

现在这些重载信息可用于pickCard调用的时候做类型检察。

注意pickCard函数只有两个重载,一个是以对象为参数的另一个是以数字为参数的。以其它类型的参数来调用将得到一个编译时错误。

4.类

发表于 2017-10-27 | 分类于 翻译 , 编程语言 , TypeScript

简介

传统JavaScript使用函数后基于原型的继承来构建可复用的组件,对于习惯于面向对象的程序员来说这种复式可能有些笨拙。从ECMAScript 2015(也叫做ES6)开始,JavaScript程序员可用基于类的方式来构建面向对象的应用。在TypeScript中,开发者也可以使用这些技术。因为这些东西会被编译成跨平台的JavaScript,所以不需要等待新版的JavaScript被普遍支持。

类

来看一个简单的基于类的例子

1
2
3
4
5
6
7
8
9
10
11
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");

这种语法和C#或Java比较相似。我们声明了一个Greeter类,头有三个成员:一个greeting属性,一个构造器,一个greet方法。
在类里面,我们通过this.的方式来访问类的成员。
最后一行,我们用new关键字来创建了一个Greeter的实例。这会调用我们前面定义的构造函数,创建一个Greeter的实例并在构造函数中初始化它。

继承

在TypeScript中,我们可以使用通用的面向对象的模式。在基于类的编程活动中,最基本的模式便是通过继承来扩展一个已经存在的类来创建新的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Animal {
name: string;
constructor(theName: string) { this.name = theName; }
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
class Horse extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters);
}
}
let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");
sam.move();
tom.move(34);

这段代码里面包含了一些关于继承的特性。在这里,我们使用extends关键字来创建一个子类。这里你可以看见Hores和Snake继承了Animal类,并能访问基类中的属性。
包合构造函数的子类必须调用通过super()来基类中的构造函数。

这个例子一演示了如何在子类中覆盖父类中的方法。Snake和Horse中都创建了move方法而覆盖了基类中的方法。尽管tom变量被声明为了Animal,而实际上是Horse,但tom.move会调用到Horse中的方法。

1
2
3
4
Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.

public,private 以及protected 修饰器

public

public 是默认的修饰器。
在我们的例子中,我们可以自由的访问我们在类中定义的成员。如果你熟悉其它语言,你也许会注意到我们并没有通过public来说明这些成员的可访问性,比如,在C#中,需要通过public来明确的说明其可公开访问。在TypeScript中,每个成员默认是public的。
你也可以明确的指定public:

1
2
3
4
5
6
7
class Animal {
public name: string;
public constructor(theName: string) { this.name = theName; }
public move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}

private

理解private.
当一个成员被标示为private,将不可以在其包含它的类外面访问了。例如:

1
2
3
4
5
6
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // Error: 'name' is private;

TypeScript的类型系统是结构化的类型系统。当我们比较两个不同的类型,不管它们从哪来,只要它们的成员是兼容的,我们就说这两种类型是兼容的。
然而,当比较的类型有private和protected的成员的时候,我们却有不同的比较方式。那么怎样的两个类型会被认为是兼容的呢?如果这两个变量中有一个变量有私有成员,那么另一个变量的私有成员必须和这个变量的私有成员在同一个地方定义(注:继承自同一个类),那么这两个变量才有可能是兼容的。对于protected也是这样的。
让我们用一个例子来说明这点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
class Rhino extends Animal {
constructor() { super("Rhino"); }
}
class Employee {
private name: string;
constructor(theName: string) { this.name = theName; }
}
let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");
animal = rhino;
animal = employee; // Error: 'Animal' and 'Employee' are not compatible

在这个例子中,我们有一个Animal类和其子类Rhino类,以及一个看起来很向Animal的Employee类(注:回想鸭类型)。我们创建这些类的实例并试着用它们相互赋值,看会发生什么。因为Animal和Rhino的private成员有相同的”出处”,所以它们是兼容的。然而Employee却不是这么回事了。当我们式着将Employee类型的变量赋值给Animal类型的变量的时候,我们会得到一个类型不兼容的错误提示。尽管Employee也有一个叫name的私有变量,但该变量却不是在Animal中定义的那个。

理解protected

protected和private是类似的,不过呢,protected修饰的成员在起子类中也是可以访问的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
protected name: string;
constructor(name: string) { this.name = name; }
}
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name);
this.department = department;
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // error

我们不能在Person类外面访问name属性,但我们可以在Employee类的方法中访问它,因为Employee继承于Person.
构造器也可用protected修饰,这表明这个类不能从外部实例化,但是可被继承。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
protected name: string;
protected constructor(theName: string) { this.name = theName; }
}
// Employee can extend Person
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name);
this.department = department;
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
let john = new Person("John"); // Error: The 'Person' constructor is protected

Readonly 修饰符

你也可以使用readonly关键字来表示一个属性是只读的。只读属性只能在其声明的地方或构造器中被初始化。

1
2
3
4
5
6
7
8
9
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor (theName: string) {
this.name = theName;
}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // error! name is readonly.

参数属性

在上一个例子中,在Octopus类中,我们声明了只读属性name 并在Octopus类的构造函数中初始化了这个属性。
这其实是一种常见的模式,参数属性可以简化这个过程,让你在同一个地方创建并初始化一个成员。

1
2
3
4
5
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {
}
}

注意我们使用readonly name:string来声明了一个参数,这会在类上创建并初始化一个name成员。这样我们就把成员的声明和赋值放在了同一个地方。
属性参数用一个前缀来声明,这个前缀可以是访问修饰符或者readonly 或这同时有这两者。使用private来声明参数属性将得到一个私有的属性,同样public,protected 声明的参数属性将得到公开或受保护的属性。

访问器

TypeScript支持getter和setter来拦截对对象成员的访问。这让你可以更好的控制对象成员是如何被访问的。
让我们把一个简单的类转化成使用get和set的类。让我们从一个没有getter和setter的类开始

1
2
3
4
5
6
7
8
class Employee {
fullName:strin
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
console.log(employee.fullName);
}

尽管让fullName可被直接访问会带来便利,但当人们可以突发奇想的修改类成员也会带来一些麻烦。
在下面的版本中,我们在修改fullName之前做一些检察以确保修改者有正确的修改密码。我们的做法是将对fullName的直接访问替换为用一个set函数来做。也相对应的添加一个get函数来使fullName可被获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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) {
console.log(employee.fullName);
}

关于访问器的说明
首先,使用访问器你需要把编译的输出目标设置为ES5或之后版。降级为ES3是不被支持的。
其次,只有get没有set的访问器会被推断为readonly.在产生.d.ts文件时这很有用,因为使用该属性的人可以知道这是一个只读的属性。

静态属性

到目前为止,我们只讨论了实例的成员(注:后半句多余,没译)。我们也可以创建类的静态成员——既那些在类上可访问的成员。在下面的例子中,我们在origin上使用static关键字,使其作为所有网格的值。所有实例通过类名.的方式来访问类成员。和this.类似,我们用Grid.来访问静态成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Grid {
static origin = {x: 0, y: 0};
calculateDistanceFromOrigin(point: {x: number; y: number;}) {
let xDist = (point.x - Grid.origin.x);
let yDist = (point.y - Grid.origin.y);
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
}
constructor (public scale: number) { }
}
let grid1 = new Grid(1.0); // 1x scale
let grid2 = new Grid(5.0); // 5x scale
console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
Abstract Classes

抽象类

抽象类常做基类,他们不能直接被实例化。可接口不同的是,抽象类可包括其成员的一些实现。使用abstract关键字来定义抽象类和抽象方法。

1
2
3
4
5
6
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log("roaming the earth...");
}
}

抽象类中的抽象方法必须在在类中被实现(注:除非子类也是抽象类)。抽象方法的语法和接口方法的语法是类似的,都是只有方法的签名而没有方法体。不同的是,抽象方法必须用abstract来修饰还可以加访问修饰符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
abstract class Department {
constructor(public name: string) {
}
printName(): void {
console.log("Department name: " + this.name);
}
abstract printMeeting(): void; // must be implemented in derived classes
}
class AccountingDepartment extends Department {
constructor() {
super("Accounting and Auditing"); // constructors in derived classes must call super()
}
printMeeting(): void {
console.log("The Accounting Department meets each Monday at 10am.");
}
generateReports(): void {
console.log("Generating accounting reports...");
}
}
let department: Department; // ok to create a reference to an abstract type
department = new Department(); // error: cannot create an instance of an abstract class
department = new AccountingDepartment(); // ok to create and assign a non-abstract subclass
department.printName();
department.printMeeting();
department.generateReports(); // error: method doesn't exist on declared abstract type

高级技术

构造函数

当你在TypeScript中声明了一个类,你实际上同时声明了很多东西。首先是类:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter: Greeter;
greeter = new Greeter("world");
console.log(greeter.greet());

当我们指定let greeter:Greeter,我们使用Greeter作为Greeter类的实例的类型,面向对象语言的程序员对此很熟悉。
我们也创建了一个叫做构造函数的东西。当我们new一个类的时候,这个函数就会被调用。为了看看实际上是什么样子,让我们看看上面的代码编译出来的JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
let Greeter = (function () {
function Greeter(message) {
this.greeting = message;
}
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
return Greeter;
})();
let greeter;
greeter = new Greeter("world");
console.log(greeter.greet());

构造函数被分配给了Greeter变量,都我们new Greeter的时候,就会调用这个构造函数并得到一个实例。构造函数上也包含了类的静态成员。另一个思考类的方式是其可分为实例侧和静态侧
修改一下前面的例子来演示其中的不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Greeter {
static standardGreeting = "Hello, there";
greeting: string;
greet() {
if (this.greeting) {
return "Hello, " + this.greeting;
}
else {
return Greeter.standardGreeting;
}
}
}
let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet());
let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";
let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());

在这个例子中,greeter1和前面的类似,我们使用Greeter类来创建它,并使用使用创建后的对象。
接下来,我们直接使用类。我们创建了一个greeterMaker变量,这个变量引用了类本身,或者说它是类的构造函数。这的typeof Greeter意思是:给我Greeter类自身的类型而不是它的一个实例。或者,更准确的说:给我那个叫Greeter的符号的类型。这个类型将包含Greeter的所有静态成员以及创建Greeter实例的构造函数。现在我们可以在greeterMaker上使用new关键字来创建Greeter的实例。

类用作接口

正如前一节所说,一个类声明创建了两个东西:一个类型和一个构造函数。因为类创建了类型,所以你可以把类用在某些能用接口的地方。

1
2
3
4
5
6
7
8
9
10
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};

3.接口

发表于 2017-10-26 | 分类于 翻译 , 编程语言 , TypeScript

Introduction

TypeScript的核心概念之一就是类型检查,Typescript的类型检查是基于值的“形状”而言的,这种类型被称为“鸭类型”或“结构化类型”(注:如果一种生物走起路来像鸭子,叫起来像鸭子,就认为它是鸭子)。在TypeScript中,是给类型“命名”的一种角色,也是种约束你的代码的有效方式。

第一个接口

关于接口最简单的说明,如下例:

1
2
3
4
5
6
function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

类型检查器检查对printLabel的调用,该函数需要一个参数,这个参数是一个有串类型的label属性的对象。调用时传递给该函数的参数实际上除了label属性,还有些其它属性。编译器只会确保有有相匹配的那些属性,但也有一些情况不是这样简单的处理。
我们可以改写这个例子,这次我们使用一个接口来描述printLabel的参数。

1
2
3
4
5
6
7
8
9
10
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

现在我们可用LabelledValue这个接口来描述printLabel的参数。我们并没有明确的让传递给printLabel的参数要实现LabelledValue这个接口,在其它语言中可能需要这样做。这里只在乎的是“形状”。如果我们传递过去的东西和LabelledValue是兼容的就可以的。

值得说明的是,类型检察器不在意属性出现的顺序,只在有意识必要的那些属性以及这些属性的类型是正确的。

可选属性

接口中并不是每个属性都是必须的,有的属性在某些情况下才会出现,甚至不会出现。在使用所谓的“option bags”(注:即把所有的选项放在一个对象里面)的模式的时候可选属性是很常用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): {color: string; area: number} {
let newSquare = {color: "white", area: 100};
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"});

有可选属性的接口的语法和普通接口是类似的,只是每个可选属性的名字后面用一个?标记出来。

可选属性的优势是你可以描述那些可能出现的属性而且避免那些没有在接口中声明的属性(注:可防止拼写错误)。比如,我们把color错写成了clor,TypeScript将给出一个错误消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = {color: "white", area: 100};
if (config.color) {
// Error: Property 'clor' does not exist on type 'SquareConfig'
newSquare.color = config.clor;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"});

只读属性

一些属性只应该在一个对象创建的时候被修改,你可以通过在属性名前加一个readonly关键字来说明这点。

1
2
3
4
interface Point {
readonly x: number;
readonly y: number;
}

你可以通过对象字面量的方式来创建Point对象,一旦创建对象后,就不再能修改x和y的值了。

1
2
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

TypeScript有一个ReadonlyArray<T>类型,基本和Array<T>一样,只不过那些修改类的方法被移除了,所以你可以确保数组在被创建之后就不再被修改了。

1
2
3
4
5
6
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

这段代码的最后一行表明,你不可以把一个ReadOnlyArray赋值给一个Array变量。
除非你使用类型断言:

1
a=ro as number[]

readonly vs const
区别使用readonly还是const的最简单的方式是看是在属性上还是在变量上。前者使用readonly后者使用const

多余属性检查

在我们的第一个例子中,我们把{ size: number; label: string; }传递给接受{ label: string; }的函数。我们也了解了可选属性,以及其在”option bags”时候的用处。
然而,这两者简单的合起来用却会遇到和JavaScript中一样的麻烦。例如:

1
2
3
4
5
6
7
8
9
10
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}
let mySquare = createSquare({ colour: "red", width: 100 });

注意这里在调用createSquare时候传递的参数中用的是colour而非color.在JavaScript中,这种错误会静悄悄的发生。你会觉得你的程序是对的:width的类型是兼容的,没有color属性,多出一个colour属性。
然而,TypeScript的立场是这也许会是程序中的一个bug。*当一个对象被赋值 给其它变量,或通过参数传递的时候,对象会被特殊对待,经过所谓的”多余属性检查“。如果该对象含有目标类型所没有的属性,就会报错:

1
2
// error: 'colour' not expected in type 'SquareConfig'
let mySquare = createSquare({ colour: "red", width: 100 });

要通过这种检查也是十分简单的,使用类型断言就可以了:

1
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

但是,一个更好的方法是添加字符串索引签名(string index signature),当然,是在如果你确定被传递的对象是可有一些额外的属性的情况下。如果SquareConfig可有其它的属性,你可以这样定义它:

1
2
3
4
5
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}

简单讨论一下索引签名。我们说SquareConfig可有任何数量的属性。只有这些属性的名字不是color和width,其类型是any。
还有一种通过检察的方式——这种放式可能会让人惊讶——把对象赋值给另外一个变量:

1
2
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

因为squareOptions不会经历多余属性检查,那么也就不会有编译错误了。

记住,对于这段简单代码,你也许会认为不会通过检察。对于更复杂的对象字面量,它们有一些方法和状态变量,所以直接传递一个对象而非对象字面量的时候,TypeScript不会对其进行多余属性检察。而以对象字面量为option bags这样的参数的时候,多余属性检察的确可以必免很多bug.这也意味着,如果你在用option bags时遇到了多余属性检察报的错误,你也许就需要修改你的类型定义。例如,对于上面的例子,如果拥有color和colour属性的对象都可以作为createSquare的参数,那么你就需要修改SquareConfig的定义了。

函数类型

TypeScript中的接口这一概念可广泛的用来描述JavaScript中的东西。除了用来描述对象及其属性,接口也能用来描述函数的类型。

为了用接口来描述函数,我们给这些接口一个调用签名。这类似于函数的声明,参数表中的每个参数都要有类型和名字。

1
2
3
interface SearchFunc {
(source: string, subString: string): boolean;
}

一旦定义了这样的接口,我们就可以像使用普通接口一样的使用它。
下面的例子演示了如何用函数接口来定义一个变量并为其赋值。

1
2
3
4
5
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}

对函数类型的类型检察不要求参数的名字相匹配:

1
2
3
4
5
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}

如果将函数赋值给指明类型的变量,例如SearchFunc,而你没有指定参数的类型,TypeScript的上下文类型系统能够推断出参数的类型。

1
2
3
4
5
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
return result > -1;
}

这里函数的返回值暗示了其类型(false或true).如果这里的返回值不是布尔类型的,TypeScript将会给出一个类型不匹配的警告。

可索引的类型

除了可用接口来描述函数,我们有可用接口来描述索引,类似于a[10]或ageMap["daniel"]。可索引的类型有一个索引签名,其用于描述我们如何来索引对象中的值,以及说明索引`和返回值的类型。
例如:

1
2
3
4
5
6
7
8
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];

这里,我们定义了一个有索引签名的StringArray接口。这个索引签名说明StringArray可用数字来作索引并返回一个string类型的值。

可用作索引的类型有string和number两种。可在同一个接口中使用这两种索引,但是数字索引的返回值类型必须是串索引的子类型。这是因为当我们用数字作为索引,JavaScript会把它转换为字符串。即用100作索引实际上和用"100"作索引是同一回事,所以我们需要这两种类型一致。

1
2
3
4
5
6
7
8
9
10
11
12
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
// Error: indexing with a 'string' will sometimes get you an Animal!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}

注:徦设TypeScript中没有这个限制,那么就会有下面演示的问题

1
2
3
4
let notOkey:NotOkay = {}
notOkey[10]=new Animal()
//TypeScript 认为notOkey["10"]为Dog,那么就会有潜在的问题
notOkey["10"]

尽管串索引是一个强有力的描述字典模式的方式,但它也强制约束了所有属性的类型。这是因为obj.prop和obj["prop"]是等价的。下面的例子中name和串索引的类型不一致,类型检察器将会给出一个错误。

1
2
3
4
5
interface NumberDictionary {
[index: string]: number;
length: number; // ok, length is a number
name: string; // error, the type of 'name' is not a subtype of the indexer
}

最后,你可以让索引是只读的:

1
2
3
4
5
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!

类类型

实现一个接口

在像C#和Java之类的语言中,接口的一个典型的用法是用来强制类要实现一些方法,TypeScript也可这样用。

1
2
3
4
5
6
7
8
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}

你也可以在接口中指定成员方法,在类中实现这些方法。

1
2
3
4
5
6
7
8
9
10
11
12
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}

接口用来描述类的公开部分。

This prohibits you from using them to check that a class also has particular types for the private side of the class instance.(每个单词都认识,就是不知道它在说什么)

静态侧类型和实例侧类型的不同之处

当使用接口和类的时候,需注意的是一个类有两个类型:静态侧类型(type of static side)和实例侧类型(type of instance side)。如果你创建了一个拥有构造函数的签名的接口,并试图用一个类来实现这个接口,那你会得到一个错误:

1
2
3
4
5
6
7
8
interface ClockConstructor {
new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}

这是因为当一个类实现一个接口的时候,只有类的实例侧会被检察。而构造函数属于静态侧,而不会被检察。
相反,你应该直接使用静态侧类型。在下面这个例子中我们定义了两个接口,用于构造函数的ClockConstructor和用于实例的ClockInterface.然后我们创建了createClock来创建传递给它的的类型的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick();
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("tick tock");
}
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

因为createClock的第一个参数是ClockConstructor类型,而AnalogClock的构造函数的类型和这个接口是兼容的,所以createClock(AnalogClock,7,32)是可以的。

扩展接口

和类一样,接口也可以扩展其它的接口。这可以让你把一个接口的属性复制到另外一个接口中。这样就可以把接口拆分成可复用的组件。

1
2
3
4
5
6
7
8
9
10
11
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;

一个接口可以扩展多个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

混合类型

就像我们前面提到的那样,接口可以描述JavaScript中的丰富的类型。由于JavaScript的动态性和灵活性,你也许会遇到一个对象,它是好几种类型混合的结果。
例如,一个东西既是一个有属性的对象又是一个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;

当你使用第三方JavaScript库的时候,你也许需要这一特性来完全描述对象的类型。

接口扩展类

当一个接口扩展只一个类,那么它继承了类的所有成员,但不包含这些成员的实现。表现的就像接口声明了这所有的接口,没有实现它们。接口甚至可以继承一到类的私有的或受保护的成员。这表明当你创建了一个继承了私有或受保护的成员,这个接口就只能被该类的子类实现。(注:原文说的是该接口只能被该类或其子类实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control implements SelectableControl {
select() { }
}
class TextBox extends Control {
}
// Error: Property 'state' is missing in type 'Image'.
class Image implements SelectableControl {
select() { }
}
class Location {
}

在上面的例子中,SelectableControl包含了Control的所有成员,包私有的state成员。state是私有变量,只能在Control的子类中实现SelectableControl,这是因为Control的子类才有这些同处声明的私有成员,这是私有成员类型兼容的必要条件。

2.变量声明

发表于 2017-10-13 | 分类于 翻译 , 编程语言 , TypeScript

变量声明

let 和const是JavaScript中的两种声明方式。就像前文说过的那样,在某些方面let和var是相似的,但是使用let可以避免许多JavaScript程序员常常遇见的陷阱。const 是对let的增强,它可以防止对变量的从新赋值。

TypeScript是JavaScript的超集,所以let和const在TypeScript中自然可用。本文将会仔细介绍这两种声明方式,。。。

var 声明方式

在JavaScript中一直(注:ES6之前)使用var关键字来声明变量。

1
var a=10;

就如你所想,这里声明了一个叫a的变量,并且赋值10给它。
我们也可以在函数中声明一个变量:

1
2
3
4
function f() {
var message = "Hello, world!";
return message;
}

我们也可以在内嵌的其它函数中访问这些变量:

1
2
3
4
5
6
7
8
9
10
function f() {
var a = 10;
return function g() {
var b = a + 1;
return b;
}
}
var g = f();
g(); // returns '11'

这段代码中,函数g捕获了变量a,当g被调用的时候,尽管这时候f的调用已经完成了,a都可以被访问,就像f中的代码访问a一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function f() {
var a = 1;
a = 2;
var b = g();
a = 3;
return b;
function g() {
return a;
}
}
f(); // returns '2'

作用域规则

var 声明方式拥有其它语言没有的奇怪的作用域(注:),举例来说:

1
2
3
4
5
6
7
8
9
10
function f(shouldInitialize: boolean) {
if (shouldInitialize) {
var x = 10;
}
return x;
}
f(true); // returns '10'
f(false); // returns 'undefined'

有些读者对于这个例子可能要多思考两次(即懵一下),变量x在if块中声明,但我们却能在if块的外面访问它(注:在常见的语言中会有一个编译时错误:x 未声明)!导致这一现象的原因是,var 方式声明的变量在其声明所在的函数、模块或全局作用域总是可见的。有人把这称为var作用域或函数作用域。函数参数也是函数作用域。

1
2
3
4
5
6
7
8
9
10
11
function sumMatrix(matrix: number[][]) {
var sum = 0;
for (var i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (var i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}

有了上面的经验,有的读者可能容易看出问题所在。最内的for循环的i覆盖了外层i的值,因为i的作用域在这整个函数!类似的bug不容易在code review时(注:代码审查,开发环节)被发现,而成为问题之源。

变量捕获的怪异行为

快速指出下列代码的输出:

1
2
3
for (var i = 0; i < 10; i++) {
setTimeout(function() { console.log(i); }, 100 * i);
}

(致不熟悉setTimeout的人:setTimeout会在指定的多少毫秒后执行指定的函数)

答案是什么呢?

1
2
3
4
5
6
7
8
9
10
10
10
10
10
10
10
10
10
10
10

许多JavaScript开发人员熟知JavaScript的这一特性,但如果你有些诧异,你也不孤单,因为很多人以为的输出是:

1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9

还记得前面提过的变量捕获吗?每一个传递给setTimeout的函数捕获了同作用域下的同一个变量i。
让我们稍稍想一下这意味着什么。setTimeout 会在若干毫秒后执行传给它的函数(注:哪怕是setTime(fn,0),请参考《异步JavaScript》),这时候for循环已经完成,i 是 10。所以,之后运行的函数(setTimeout的回调)输出都是10。

一个常用的解决方案是在每次循环中用立即执行函数表达式(IIFE,Immediately Invoked Function Expression)来捕获i

1
2
3
4
5
6
7
for (var i = 0; i < 10; i++) {
// capture the current state of 'i'
// by invoking a function with its current value
(function(i) {//在了的i覆盖了上层作用域的i
setTimeout(function() { console.log(i); }, 100 * i);
})(i);
}

这种看起来奇奇怪怪的模式实际上是非常通用的。参数i实际上覆盖了for循环中的i,只是名字相同而矣(注:若理解起来别扭,把IIFE内部的i改成j)。

如下

1
2
3
4
5
6
7
for (var i = 0; i < 10; i++) {
// capture the current state of 'i'
// by invoking a function with its current value
(function(j) {
setTimeout(function() { console.log(j); }, 100 * j);
})(i);
}

let 方式声明

现在,你已经知道var的这些问题了,这也正是let被引入的原因,除了关键字不同,两种声明的写法是一样的:

1
let hello = "Hello!";

关键的区别不在于语法,而在于语义——我们即将介绍。

块作用域

当一个变量用let声明后,它使用所谓的词法作用域(或称块作用域).不同于var 会将作用域“泄露”到其所在函数,块作用域只在其块内可见,例如for循环体。

1
2
3
4
5
6
7
8
9
10
11
12
function f(input: boolean) {
let a = 100;
if (input) {
// Still okay to reference 'a'
let b = a + 1;
return b;
}
// Error: 'b' doesn't exist here
return b;
}

在段代码中,我们声明了两个变量——a、b,a的作用域在f的函数体内,而b的作用域在if的语句块内。

在catch语句中声明的变量也有自己的块作用域:

1
2
3
4
5
6
7
8
9
try {
throw "oh no!";
}
catch (e) {
console.log("Oh well.");
}
// Error: 'e' doesn't exist here
console.log(e);

块作用域变量的另一个性质是在其声明前是不可以被读写的。(注:有句废话没翻译)

1
2
a++; // illegal to use 'a' before it's declared;
let a;

值得注意的是,对于块级变量,你仍可以在声明前捕获它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function aFun(){
function foo(){
console.log(a);
}
// illegal call 'foo' before 'a' is declared
// runtimes should throw an error here
/*
* 注:是否有runtime错误要看tsc编译参数,如果编译成ES5及更早版本则没有runtime错误
* 请对比(徦设文件名为test.ts):
* tsc -t ES5 test.ts
* tsc -t ES2016 test.ts
*/
foo();
let a=1;
return foo;
}

重声明以及覆盖

上文提及的var方式声明的变量可以在同一作用域被声明多次,而不会报错:

1
2
3
4
5
6
7
8
function f(x) {
var x;
var x;
if (true) {
var x;
}
}

在这个例子中,所有对x的声明都指向了同一个x,这样做的后果常常是成为了bug之源。因此,let不允许这样的声明。

1
2
let x = 10;
let x = 20; // error: can't re-declare 'x' in the same scope

TypeScript 会告诉我们,同一个块作用域不能有两个同名的变量。

1
2
3
4
5
6
7
8
function f(x) {
let x = 100; // error: interferes with parameter declaration
}
function g() {
let x = 100;
var x = 100; // error: can't have both declarations of 'x'
}

当然如果是父——子作用域是可以的:

1
2
3
4
5
6
7
8
9
10
11
function f(condition, x) {
if (condition) {
let x = 100;
return x;
}
return x;
}
f(false, 0); // returns '0'
f(true, 0); // returns '100'

这种在子作用域声明一个和父作用域同名的变量的形为就是——变量覆盖(shadowing)。这是一把双刃剑,在“不小心”覆盖了另一个变量的时候就可能会引入一些bug,同时变量覆盖也能访止一些bug,如前面的sumMatrix

1
2
3
4
5
6
7
8
9
10
11
function sumMatrix(matrix: number[][]) {
let sum = 0;
for (let i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (let i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}

这段代码是能够完成矩阵求和的,因为内层for循环的i覆盖了外层循环的i,那么外层的i就不会被意外的修改了。

在意于写清晰明了的代码的时候应该必免使用变量覆盖这一特性。尽管在有些场合,这一特性会带来一些优势,你最好仔细考量。

块作用域变量捕获

当我们首次了解var变量的捕获的情况的时候,我们粗略的调查了变量
被捕获后的形为。详细说来,每当一个作用域(中的代码)在执行的时候,有一个包含变量的“环境”被创建出来。这个环境和其捕获的变量在其所在的作用域执行完成后依然可以存在!

1
2
3
4
5
6
7
8
9
10
11
12
function theCityThatAlwaysSleeps() {
let getCity;
if (true) {
let city = "Seattle";
getCity = function() {
return city;
}
}
return getCity();
}

因为我们捕获了city变量,所以在if块执行之后我们仍可以访问它。
回想前面setTimeout那个例子,我们需要使用IIFE来捕获变量。
实际上,我们创建了一个新的变量环境来捕获变绿。IIFE的方式写起来有些老火,幸运的是在TypeScript中不必那样。
let方式在循环语句中声明的变量很是不同,除了创建一个新的环境,这类声明(指let和const)也会在每次循环的时候创建一个新的作用域,这正是我们使用IIFE所作的事情。修改一下setTimeout那个例子,使用let来声明变量:

1
2
3
for (let i = 0; i < 10 ; i++) {
setTimeout(function() { console.log(i); }, 100 * i);
}

输出则入我们所想的那样:

1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9

const 声明

const是声明变量的另一种形式:

1
const numLivesForCat = 9;

它和let很像,但是,就像其名字所暗示的那样,其值是不能够被修改的。换句话说,const和let具有一样的作用域规则,但是你不能为之重赋值。

可别和不可变(immutable)相混淆:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const numLivesForCat = 9;
const kitty = {
name: "Aurora",
numLives: numLivesForCat,
}
// Error
kitty = {
name: "Danielle",
numLives: numLivesForCat
};
// all "okay"
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;

除非你采取了特殊措施,const变量的内部状态(注:对像的属性,数组的元素等)是可变的(注:const 变量指的是引用不变)。所幸,TypeScript可指定属性为readonly来避免属性被修改。接口章会详述这个问题。

let vs. const

设计两种具有相似作用域语义的变量声明方式,这使我们自然会问“使用哪个?”。答案和很多问题一样:看情况。

根据最少特权原则,所有无计划修改的变量应声明成const。原因是,如果一个变量不需要被修改,那么其头代码也不应有能修改这些变量的能力,也需要思考它们是否真得需要重赋值这些变量。使用const也使的在推算数据流的时候(注:即在脑中执行代码)代码的行为更可被预测。

[placeholder]

解构

TypeScript拥有的es2015的另一个特性是解构。[这里]是关于解构的完整说明,本节将大略的描述解构。

数组解构

最简单的数组解构是数组的解构赋值。

1
2
3
4
let input = [1, 2];
let [first, second] = input;
console.log(first); // outputs 1
console.log(second); // outputs 2

这里创建了两个变量,first和second,和使用索引的方式效果一个,但是更加方便。

1
2
first = input[0];
second = input[1];

解构赋值也可使用在已经声明的变量上,

1
2
// swap variables
[first, second] = [second, first];

以及用在函数的参数中:

1
2
3
4
5
function f([first, second]: [number, number]) {
console.log(first);
console.log(second);
}
f([1, 2]);

你可以使用…语法来创建一个list变量来保存剩余元素:

1
2
3
let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // outputs 1
console.log(rest); // outputs [ 2, 3, 4 ]

当然,你也可以忽略你不关心的尾部的元素。

1
2
let [first] = [1, 2, 3, 4];
console.log(first); // outputs 1

当然,其它元素也是可忽略的:

1
let [, second, , fourth] = [1, 2, 3, 4];

对象解构

你也可以解构对象:

1
2
3
4
5
6
let o = {
a: "foo",
b: 12,
c: "bar"
};
let { a, b } = o;

这里从a对象创建了a、b变量,其值分别是o.a,o.b并忽略了a.c。

和数组解构一样你也可以不要声明而赋值:

1
({ a, b } = { a: "baz", b: 101 });

注意,我们必须用园括号把代码包起来,js会把{作为块来解析
你也可以使用...语法来创建一个包含其它未被解构属性的变量

1
2
let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;

属性重命名

你也可以给属性一个不同的名字

1
let { a: newName1, b: newName2 } = o;

这种语法开始让人有点迷糊,你可以把a:newName1解释为newName1是a,这和下面的代码是等价的:

1
2
let newName1 = o.a;
let newName2 = o.b;

这里的:不是表示类型,如果你想指定变量的类型,那么需要在整个解构后。

默认值

默认值可以在某个属性是undefined的时候指定一个默认的值。

1
2
3
function keepWholeObject(wholeObject: { a: string, b?: number }) {
let { a, b = 1001 } = wholeObject;
}

这里,在函数keepWholeObject中有变量:a,b,以及wholeObject。

函数声明

解构也可用在函数的声明中,如下例:

1
2
3
4
type C = { a: string, b?: number }
function f({ a, b }: C): void {
// ...
}

[placeholder]

延展

不知道spread对应的术语,乱译为延展

spread运算和解构运算作用是相对的,它可以把一个数组中的元素放到其他数组中。或者把一个对象的属性放到其它对象中。
比如:

1
2
3
let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];

bothPlus数组为[0,1,2,3,4,5],Spread创建了first和second的浅拷贝
你也可以spread一个对象

1
2
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };

search为{ food: "rich", price: "$$", ambiance: "noisy" },对象的延展比数组的延展要复杂一些。和数组延展一样,处理的过程是“从左到右的”,只是处理的结果仍然是一个数组而矣。这意味着,后出现的属性将覆盖先出现的属性。所以,如果我们修改上面的例子:

1
2
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { food: "rich", ...defaults };

那么得到的search的food属性的值将会是spicy。

对象延展还有一些限制,结果只会包含对象自身的、可枚举的属性(注:自身的表示非原型链上的属性)
所以,当你延展一个对象时,将丢失其上的方法。

1
2
3
4
5
6
7
8
9
class C {
p = 12;
m() {
}
}
let c = new C();
let clone = { ...c };
clone.p; // ok
clone.m(); // error!

此外,Typescript编译器不允许泛型函数的类型参数。这一特型在将来的版本中可能会受支持。

1.基本类型

发表于 2017-10-10 | 分类于 翻译 , 编程语言 , TypeScript

Boolean

最基本的数据类型为简单的true/false值,在TypeScript中被称为boolean 值。

1
let isDone:boolean = false;

Number

和JavaScript 一样,所有数字都是浮点数,即number类型
除了十六进制和十进制字面量,TypeScript也支持ECMAScript 2015引入的二进制和八进制字面量

1
2
3
4
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;

String

在JavaScript中,编写程序的一项基本工作是对文本数据的处理。和其它的语言一样,我们使用string来表示这些文本数据类型。和JavaScript一样,TypeScript也使用双引号和单引号来表示字符串数据。

1
2
let color: string = "blue";
color = 'red';

你也可以使用模版字符串,在模版字符串中可以嵌入表达式,同时,模版串内可以换行。模版串用反引号来表示,其内嵌的表达式形如${ expression }

1
2
3
4
5
let fullName: string = `Bob Bobbington`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ fullName }.
I'll be ${ age + 1 } years old next month.`;

这和按如下方式定义sentence 等价:

1
2
let sentence: string = "Hello, my name is " + fullName + ".\n\n" +
"I'll be " + (age + 1) + " years old next month.";

数组

和JavaScript一样,TypeScript允许你使用数组。数组的类型可有两种写法。
第一种是在元素类型的后面跟上一对方括号:

1
let list:number[] = [1,2,3]

第二种是使用泛型数组参数:

1
let list:Array<number> = [1,2,3]

元组(Tuple)

一个数组元素个数固定且类型已知,用元组来表示。例如,你可能需要表示一个string和一个number构成的二元组

1
2
3
4
5
6
// Declare a tuple type
let x: [string, number];
// Initialize it
x = ["hello", 10]; // OK
// Initialize it incorrectly
x = [10, "hello"]; // Error

当我们访问一个索引已知的元组元素,那么这个元素的类型也就是已知的了。

1
2
console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr' typescript 知道x[1]是一个number类型的东西

当访问元组索引范围外的元素的时候,TypeScript会将其视为该元组已知类型的并类型(Union types).

1
2
3
4
5
x[3] = "world"; // OK, 'string' can be assigned to 'string | number'
console.log(x[5].toString()); // OK, 'string' and 'number' both have 'toString'
x[6] = true; // Error, 'boolean' isn't 'string | number'

并类型是一个高级话题,将在后文讨论。

枚举

除了JavaScript的数据类型,TypeScript还增加了一种有用的类型——枚举(enum). 和C#一样,枚举是给一组数字取一个友好的名字的方式。(注: typescript 2.4 中枚举也可以是字符串)

1
2
enum Color {Red, Green, Blue}
let c: Color = Color.Green;

默认情况下,枚举值从0开始(后续加1)。你可以手动为枚举的某个成员设置一个值来改变其默认的值。

1
2
enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green; // 2

或者,可以为枚举的每个成员设置值。

1
2
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

一个方便的特性是你根据枚举值得到其对应的名字.

1
2
3
4
enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];
console.log(colorName); // Green

Any 类型

我们也许需要描述这样一种变量——我们在写程序的时候并不知道其类型,这些变量值可能来自一些动态的内容,例如——第三方库。这种情况,我们想对这些变量停用类型检察以便通过编译。为了做到这点,我们使用any 类型

1
2
3
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

any 类型是和JavaScript库交互的有效方式,这可让你逐渐的启用或停用类型检察。你也许希望Object类现会扮演同样的角色,在其它某些语言中的却如此。但在typescript中,Object类型的变量只允许你将any类型的变量赋值给它,而不能调用上面的任意方法,那怕是的确存在的方法。

1
2
3
4
5
6
let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)
let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.

Void 类型

void有些像是和any相反,它表示——没有类型。你可能常在函数的返回值类型声明的地方见到该类型。

1
2
3
function warnUser(): void {
alert("This is my warning message");
}

用void来声明变量就没什么意义了,因为你只能给它赋undefined或null给它。

Null 和 Undefined

默认情况下,null和undefined 是所有其它类型的子类型,所以null和undefined可以赋值给任何变量。
但当--strictNullChecks选项启用后,null和undefined就只能够赋值给void或其对应的类型(注:null,undefined 即是类型,也是值)。这有助于避免多数常见错误。
当你希望传递string或null或undefined的类型的值的时候则需要使用并类型 string | null | undefined。

推荐使用--strictNullChecks,但该教程徦设该选项是关闭了的。

Never 类型

never 类型代表了“永远不会出现”的类型,never 是那些总是抛出错误或永不返回的函数的“返回类型”()。
(Variables also acquire the type never when narrowed by any type guards that can never be true. ???)
never 类型也是所有类型的子类型,同时,没有类型是never类型的子类型,所以除了never自己,没有类型可以赋值给never。 any也不可以赋值给never。
函数返回类型为never的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Function returning never must have unreachable end point
function error(message: string): never {
throw new Error(message);
}
// Inferred return type is never
function fail() {
return error("Something failed");
}
// Function returning never must have unreachable end point
function infiniteLoop(): never {
while (true) {
}
}

类型断言

有时候你会遇到这样的情况:你比TypeScript更清楚一个值是什么类型的。
类型断言是一种指定类型的方法,相当于告诉编译器:“相信我,我知道为在做什么”。类型断言和其它语言中的类型转换(type cast)是类似的,但不会执行特别的检察或数据重构操作。也没有运行时的开销,只是纯粹的为编译器使用而矣。TypeScript徦设你已经做过必要的检察了。
类型断言有两种形式,一种是尖括号语法:

1
2
3
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

另一种是as语法:

1
2
3
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

这两种方式是等价的,使用哪种取决于你的偏好。但是,如果在TypeScript中使用JSX, 则只有as方式是可用的(注:JSX 用尖括号了表示组件,这会引入二意性)。

关于 let

你可能注意到了,我们使用let关键字代替了JavaScript中的var关键字。let 实际上是JavaScript 新标准的东西,TypeScript使其提前可用了。后面我们会进一步讨论let,let可以缓解JavaScript中的很多问题,所以,尽可能使用let来代替var吧!

进一步阅读

never 和 void 的区别

Qianba

Qianba

7 日志
3 分类
1 标签
© 2017 Qianba
由 Hexo 强力驱动
主题 - NexT.Muse