0%

低成本DIY一把小红点机械键盘

事情是这样开始的,有一段时间“小红点” 相关的话题老是出现在我的视野里面,我本就是vim用户,使用鼠标的频率本来就不高,手老是在键盘和鼠标之间切换也挺烦的。于是就想尝试一下小红点键盘。这几年一直使用机械键盘,所以也不太想尝试联想的小红点薄膜键盘。简单了解了下,似乎处理Yoda系列再没有其它的小红点机械键盘了。Yoda II 看起来不错,颜值在线,可惜价格太贵,而且看起来也不太好买,或许需要海淘。于是试着去闲鱼逛逛。闲鱼上到是有人在卖DIY的小红点键盘呢,可是发给卖家的消息有如泥牛入海。后来混进卖家的群才得知卖家不想继续这门生意。

于是想要不自己做吧,正好手里有把闲置的RK61。

经过一番折腾,搞出来了。因为我本身不是硬件工程师,对电子或则硬件知之甚少,所以我没有设计PCB板(也不会),直接就是STM32开发板加小红点模块加键盘一焊了事。

元件以及工具

RK61 一把。

STM32F103T8 开发板一片,选这款开发板的原因是它便宜,IO口数量也够用,而且其对Ardunio有官方支持。

SSD1306 128x64 IIC 液晶屏幕一块,这个也可以不要。添加这样一块屏幕的目的有两个,一是用来显示一些状态信息,用RK61键盘的一个痛点就是很容易不小小搞到了其它的模式而不得不重置键盘,使得使用体验很糟糕;二是将来可以通过这块屏幕来调节小红点的参数。当然,如果不要这块屏幕的话,固件代码要稍作调整。

小红点模块一个。这个可以在闲鱼上买联想电脑的拆机模块,大概几块钱一个。我买了5个,模块上的芯片型号都是PTMP754DR。这些模块的排线有的是8根,有的是10根,而10根的排线里有两根并未使用到。最好直接买到8根的,那样就不用修剪了。

FPC 转接板8pin,规格1mm,两个。这个板子是用来将小红点模块连接到开发板上的。

FPC 转接板20pin,两个。这个板子是用来连接键盘PCB和开发板的。为了省事,我直接使用了RK 61自己的PCB板,我将RK61上面的主控芯片拆了下来,将键盘的行线和列线焊接到了FPC转接板。

FPC 转接线20pin,用来连接键盘和开发板。

开关二极管两三个。因为会在PCB上打个洞,这个洞会打断一些列线,因此我们需要补两个二极管。

细导线若干。作飞线用,细点好,我买的0.6mm的。如果你对烙铁不熟悉,千万别用漆包线代替,那玩意太难焊了。

漆包线若干。我用的0.25MM的。细导线再细还是有点粗,用来焊接20pin的FPC到STM32可能显得有点挤,这里其实可可以用漆包线来焊,因为STM32和FPC两边都有孔相对好焊一点。

微动开关三个。用作鼠标左、中、右三键。

烙铁一把。

ST-Link下载器一个。用来下载程序。

USB 转TTL一个,可选,用来打印调试信息。

杜邦线6条(母头)。用来留出TTL和ST-Link的接口,让我们在键盘安装完毕后还可以下载和调试程序。

USB公头、母头个一个,可选。如果你设计的合适也可直接使用开发板上的USB接口作为最终的USB接口。当然,有USB公头和母头让你在安排开发板的位置的时候更加灵活一些。

小电磨机一个。用来在RK61的外壳上打孔。

电钻,用来给小红点的杆打洞。因为RK61的定位板是钢的,因此这个孔比较难钻。

小红帽,拼多多或淘宝搜索小红点可以买到。

中性笔芯一支。用做小红点的杆。我运气比较好,找的第一支的粗细就刚刚好。

万用表,可选。测试连通性。

热熔胶。

焊接与安装

Untitled

用电钻在GHB这三个键之间打个孔,强调下是GHB,因为我第一次就打在了HJN之间,做完才发现。这也是我做了两把小红点键盘的原因之一。

键盘焊接

2023-11-09_16-18.png

将RK61键盘拆开,如上图摆放,按键的两个焊点中靠上的一个是行线,靠下一个连接到了一个二极管的负极。上图右侧有4列额外焊的列线,实际上是不需要的,因为我一开始没有想到列线也可以就用板子原生的列线,实际上每列的二极管的正级是相互连通的。我将板子上原来的主控芯片拆掉了,因为我担心会有影响。实际有没有影响我也不知道。

按键之间的电路图如下,为了简单只画了6个键:

Untitled

然后按从上到下的顺序将行线焊接到FPC转接板的1-5号孔,按从右到左的顺序将列线焊接到FPC转接板的6-19号孔。另一个FPC 转接板将被焊接到STM开发板,分别将1-5号孔通过飞线焊接到:PB0,PA7,PA6, PA5, PA4. 分别将6-19号孔焊接到:

PA3 // 6 col0

,PA2 // 7 col1

,PA1 // 8 col2

,PA0 // 9 col3

,PC15 // 10 col4
,PC14 //11 col5

,PC13 // 12 col6
,PB15 // 13 col8

,PA8 // 14 col7 B15
//,PA11 // 13 col7
,PA15 // 15 col9

,PB3 // 16 col10

,PB4

,PB5

,PB14

这样键盘和STM开发板就可以通过FPC排线连接起来了。当然也可以不按这个顺序焊接,只要保证键盘的1-5行(从上往下)分别被连接到PB0,PA7,PA6, PA5, PA4,(列也类似)即可。

我这里焊好后是这样的:

Untitled

小红点模块的焊接

小红点模块主控芯片引脚定义:

Untitled

就我买到的模块而言,将模块排线的金手指朝上,那么从左到右的8根线分为为:

VCC, CLK(Clock),Button Right, Button Left, Button Middle,GND (Ground), RST, Data

如果排线有10根,那么最左边两根没有用到,其它的线序和上面相同。我不确定是不是所有型号的模块都是这个次序,所以你最后用万用表测试一下保险些。用万用表测试连通性的档位来测量排线和哪个Pin相连即可。

小红点模块和STM开发板连接如下图:

Untitled

GND都连接到STM的GND上,VCC连接到STM的5v处。Button L,R,M 分别连接到三个微动开关,开关的另一极连接到GND,这三个微动开关即鼠标的左右中键。需要注意的是,三个微动开关安装的位置要合适,这个需要你按照你自己的实际情况来布置。

屏幕的焊接

屏幕有四根线:GND,VCC,SCK,SDA,VCC连接到5v,GND连接到STM的GND, SCK连接到PB6, SDA连接到PB7。

这里的内部布局是这样的:

Untitled

我这里的小红点模块和STM32开发板是直接焊到一起的,而没有使用转接板。因为这个模块的排线焊点间距比较大,好焊。一般年代越近的笔记本拆出来的模块越精密,排线焊点比较小,这时候用转接板就比较方便了。

下载接口的焊接

STM32 开发板尾部有4根弯的排针,它们是用来连接STLink下载器的。我们可以在合适的地方开孔,将杜邦线焊接到这四根排针上。如果不嫌弃麻烦,也可以将这几根针拆下来。

USB 延长线的焊接和安装

USB公头的D+,D-,GND,VCC 分别通过长度合适的导线焊接到USB 母头的D+,D-,GND,VCC上。并将USB公头插入STM32开发板的USB接口上。在键盘底壳合适的地方开孔,并将USB母头用热融胶固定。

固件的编译和烧录

克隆这个项目到本地:

git clone https://github.com/JunYang-tes/hello-keyboard

用VScode打开这个hello-keyboard 下面的fireware 目录。并在VScode里装上Arduino这个插件。

按Ctrl+Shift+P,打开VScode的命令面板,输入Open User Setting, 打开settings.json, 往这个文件中添加:

"arduino.useArduinoCli": true,
"arduino.additionalUrls": [
   "https://github.com/stm32duino/BoardManagerFiles/raw/main/package_stmicroelectronics_index.json"
 ],

按Ctrl+Shift+P,输入 Board Manager, 按Enter打开Arduino Board Manager, 点击界面上的Refresh Package Indexes.

Untitled

搜索STM32, 选择2.7.0 点击Install。

一切就绪后按Ctrl+Shift+P 输入Verify, 即可开始编译固件。如果一切没问题的话,编程会生成fireware.ino.bin, 相对repo的完整路径是hello-keyboard/fireware/build/fireware.ino.bin

通过STLink 下载器,将键盘连接到PC,打开**STM32CubeProgrammer** 即可将固件下载到键盘。

Untitled

断开STLink, 通过USB连接键盘到PC,应该就能正常的工作了。

如果键盘没有反应,但是电脑识别了HID设备,那么多半是小红点模块或者模块的连接有问题。因为相关代码在读小红点模块的数据的时候会等待Clock信号的变换,如果没等到就死在那里了。

在linux 系统中查看系统是否识别到键盘盘的命令如下:

lsusb | grep STM

如果系统识别了键盘,应该会看见类似下面的输出:

Bus 003 Device 005: ID 0483:5711 STMicroelectronics BLUEPILL_F103C8 HID in FS Mode

条件类型

条件类型是什么

Typescript 2.8 引入了条件类型

条件类型是一个类型层面的表达式,其语法和作用都类似于三目运算。

type T = <condition> ? <satisfy> : <non-satisfy>

在条件满足的时候计算的类型为表达式的类型,否则为 的类型。到目前为止,condiation 部分只支持extends 一种运算。

条件类型常与泛型一起使用:

type IsNumber<a> = a extends number ? true : false
type A = IsNumber<1> // 计算为true
type B = IsNumber<{}>// 计算为false

extends 字面意思是扩展、继承的意思,a extends b 可读做为 a 继承于 b,在条件类型的语境下,可读作a继承于b吗?,因此上面的例子可读作:a 继承于number 吗?如果是,那么IsNumber计算为true,否则计算为false。

那么如何判断a 是否继承于 b 呢?就是看a类型的值是否可以赋值给b类型,或者接受b类型作为参数的函数是否可以接受a类型。如果a 继承于 b 那么 b 是父类型,a 是子类型。a相对于b更具体,b相对于a更抽象。a 的值域是b的值域的子集(非真子集)。

type A = 1|2 extends number ? true : false //true
type B = 1|2 extends 1|2|3 ? true : false // true

记 1|2 这个联合类型为a,number 为 b, a extends b 是满足的,因为接受number 为参数的地方,也可依接受1或2, 反之则未必。

条件类型的使用例子

如何用条件类型实现一个判断类型是否一样的泛型类型呢?

type Equal<A,B> = ?
type T1 = Equal<number,number> //期望: true
type T2 = Equal<string,number> //期望: false
type T3 = Equal<1|2,2|1> // 期望: true
interface Animal {
  live(): void;
}
interface Dog extends Animal {
  woof(): void;
}
type T4 = Equal<Animal,Dog> // 期望: false

对于两个类型A和B,如何定于其是否一样呢?我认为可以这样:如果A类型的值可以赋值给B类型的变量,同时B类型的值可以赋值给A类型的变量,那么A和B类型是一样的。或者说,A类型的取值范围和B类型的取值范围一模一样,那么A和B类型是一样的。

type A = 1 | 2
type B = 2 | 1
declare let a:A
declare let b:B
a = b;//OK
b = a;//OK

这样,很容易想到一个实现:

type Equal<A,B> = A extends B
 ? (B extends A ? true : false)
 : false

既当A extends B 并且 B 也 extends A 的时候 A 和 B 是一样的。

type Equal<A,B> = A extends B
 ? (B extends A ? true : false)
 : false

type T1 = Equal<number,number> //期望: true  
type T2 = Equal<string,number> //期望: false
type T3 = Equal<1|2,2|1> //期望: true
//   ^ boolean
interface Animal {
  live(): void;
}
interface Dog extends Animal {
  woof(): void;
}
type T4 = Equal<Animal,Dog> //期望: false

T3 计算出来的类型为boolean 而不是 true, 这是怎么回事呢?这是因为条件类型遇到联合类型的时候是先进行分配后计算的(见Distributive Contional types)。

// T3 = Equal<1|2,2|> 
// => Equal<1,2|1> | Equal<2,2|1>
// => Equal<1,2>|Equal<1,1>  |   Equal<2,2>|Equal<2,1>
// => false     |true        |   true      |false
// => boolean

可以通过加方括号来防止分配:

type Equal<A,B> = [A] extends [B]
 ? ([B] extends [A] ? true : false)
 : false

为什么方括号可以防止分配呢,因为[A] 形成了新类型:一个单元素的元组,没有了联合类型自然就没有了分配。

按照前面对类型一样的定义,Equal<any, 1> 是返回true的,可如果我们想给any 开个后门,只让Equal<any,any> 返回true,其它情况返回false,怎么做呢?加入有个IsAny 就好了

type IsAny<A> = ?
type BothAny<A,B> = [IsAny<A>,IsAny<B>] extends [true,true] ? true : false
type Equal<A,B> = BothAny<A,B> extends true ? true :
[A] extends [B] 
? ([B] extends [A] ? true : false)
: false

IsAny 当然不能用 A extends any 来做,因为无论A是什么类型,A extends any 总是成立。但是我们可以这样:

type IsAny<A> = (<T>()=>T extends A ? "whatever" :"whatsoever") extends
(()=> "whatever") ? true : false

因为只有A为any的时候 (<T>()⇒T extends A ? "whatever" : "whatsover") 被计算为 (<T>()=>T extends any "whatever")进一步被计算为(<T>()=>"whatever")最后整个表达式被计算为true。

这里还有另外一中做法:

export type Equals<X, Y> =
    (<T>() => T extends X ? 1 : 2) extends
    (<T>() => T extends Y ? 1 : 2) ? true : false;

手中有一块STM开发板,其型号时STM32F103C6T6A。想玩玩Rust嵌入式开发,那么Hello world级别的程序应当是点亮板载的LED灯了。

那么如何建立起一个Rust的嵌入式开发环境呢?参考了不少资料,踩了几各坑,最终完成这一目标。这些资料大都需要你有一个仿真器,而且会涉及到使用openOCD,相对麻烦一点。这这是一个简单的hello world程序,我并不希望设置一系列的工具链来弄好一个并不会被使用到的调试功能。我只想简单的看到板载LED被点亮而已。因此本篇的目的是设置好开发环境,使用下载器来下载编译好的程序。

环境

首先的首先,你得有安装好rust,这个按下不表。

cargo new led 初识化一个项目,编辑Cargo.toml 加入必要的依赖:

[dependencies]
embedded-hal = "0.2.7"
nb = "1"
cortex-m = "0.7.6"
cortex-m-rt = "0.7.1"
# cortex-m-semihosting = "0.5.0"
panic-halt = "0.2.0"
[dependencies.stm32f1xx-hal]
version = "0.10.0"
features = ["rt", "stm32f103"]

编辑src/main.rs:

#![allow(clippy::empty_loop)]
#![deny(unsafe_code)]
#![no_main]
#![no_std]

use panic_halt as _;

use cortex_m_rt::entry;
use stm32f1xx_hal::{pac, prelude::*};

#[entry]
fn main() -> ! {
    let p = pac::Peripherals::take().unwrap();

    let mut gpioc = p.GPIOC.split();

    gpioc.pc13.into_push_pull_output(&mut gpioc.crh).set_low();

    loop {}
}

如果这时候编译,会得到一大片错误:

Untitled

其中最后以个是最主要的,因为我们声明了no_std, 但是没有制定变异的目标。因此编译器提示我们加上—target 参数或者添加.cargo/config 文件。

STM32F1XX 的CPU 是Cortex-M3,通过网络资料可知道,编译目标是: thumbv7m-none-eabi,但先要安装相应的依赖:

rustup target add thumbv7m-none-eabi

然后执行 cargo build --release --target thumbv7m-none-eabi 不知道为什么cargo没有自动下载依赖,导致了编译失败。于是我主动执行了一下cargo add panic-halt然后再build,就可以了。

为了避免老是需要指定target为thumbv7m-none-eabi,让我们把它写入.cargo/config

[build]
target = "thumbv7m-none-eabi"

问题是编译生成的文件太大了,有900多K,要知道这个开发板子的Flash才32K。

cargo-binutils 提供了一系列工具,其中cargo-size 可以查看文件的大小信息,让我们先安装它:cargo install cargo-binutils

然后执行:cargo size --bin led -- -A (如果没的.cargo/config, 这里也需要加上 —target),这个命令提示我们需要设置好llvm-tools,于是执行rustup component add llvm-tools-preview

Untitled

可以看出绝大不部分都是debug信息,问题是release版本里为什么有这么多debug信息?还有一个更严重的问题:没有代码段(.text)!

rust embeded no text section为关键字搜索发现应该是链接器的配置不对,改.cargo/config 如下:

[target.thumbv7m-none-eabi]
rustflags = [
  "-C", "link-arg=-Tlink.x",
]

[build]
target = "thumbv7m-none-eabi"

再编译,提示linker需要知道内存布局是怎么样的,这个布局由memory.x 指定,STM32F103C 系列的布局是这样的:

MEMORY
{
  FLASH : ORIGIN = 0x08000000, LENGTH = 32K
  RAM : ORIGIN = 0x20000000, LENGTH = 10K
}

意思是Flash从0x08000000处开始,长32K. RAW从0x20000000开始,长10K.

现在的目录结构是这样的:

Untitled

这下编译后查看size就有代码段了。

通过执行cargo strip --bin led --release -- --strip-all 可以移除里面的调试信息,这里有个小坑:cargo strip —help 给出的例子是错的:

Untitled

可是strip完还是有129K,远大于F103的Flash大小。

Untitled

百思不得其解,互连网都翻烂了才发现这里生成的是ELF文件,而下载到单片机的是Binary文件。objcopy 可以将ELF 转为bin文件:

cargo objcopy --bin led --release -- -O binary out.bin

Untitled

下载

ST-LINK 接线顺序

:TODO

用STM自家才STMCubeProgrammer来下载固件,必需要吐槽一下这个GUI,java做的启动巨慢,用的java1.8,在高分屏下字体贼小。

Untitled

受不了这个GUI,可以直接使用STM_Programm_CLI 下载:

STM_Programmer_CLI --list #显示可用的连接方式

Untitled

用下面的命令下载:

STM32_Programmer_CLI -c port=SWD <path-to-bin> 0x08000000

下载完成后再下板子上的Reset就可以看见板载的LED亮了(出厂设置为该灯闪烁)。

https://github.com/rust-embedded/discovery/issues/147

arm-none-eabi-objcopy -O binary t t1

总之体验糟糕极了。

sdcc 在编译下面代码时候报警告:

#include <8052.h>
#define addr0 P1_0
#define addr1 P1_1
#define addr2 P1_2
#define select_38_0 (addr0=0,addr1=0,addr2=0)
#define select_38_1 (addr0=1,addr1=0,addr2=0)
#define select_38_2 (addr0=0,addr1=1,addr2=0)
#define select_38_3 (addr0=1,addr1=1,addr2=0)
#define select_38_4 (addr0=0,addr1=0,addr2=1)
#define select_38_5 (addr0=1,addr1=0,addr2=1)
#define select_38_6 (addr0=0,addr1=1,addr2=1)
#define select_38_7 (addr0=1,addr1=1,addr2=1)
#define select_38(n) \
  switch (n) { \
    case 0: \
      select_38_0; \
      break; \
    case 1: \
      select_38_1; \
      break; \
    case 2: \
      select_38_2; \
      break; \
    case 3: \
      select_38_3; \
      break; \
    case 4: \
      select_38_4; \
      break; \
    case 5: \
      select_38_5; \
      break; \
    case 6: \
      select_38_6; \
      break; \
    case 7: \
      select_38_7; \
      break; \
  }
#define LED P0_0
void delay(void) {
  int i, j;
  for (i = 0; i < 0xff; i++) {
    for (j = 0; j < 0xff; j++)
      ;
  }
}

void main(void) {
  LED = 0;
  char i = 0;
  while (1) {
    delay();
    select_38(i);
    i = (i + 1) % 8;
  }
}

出现这样的警告:

a.c:54: warning 110: conditional flow changed by optimizer: so said EVELYN the modified DOG
a.c:54: warning 126: unreachable code

这段代码是一个流水灯的实验,上面编译出来的文件的执行效果是第一个灯常亮。从编译器给出的警告来看,54行发生了某种优化,导致出现了不可达的代码。因此i没有变换,所以没有流水灯的效果。于是我尝试交换了54和55行的代码,结果正常通过了编译。并且执行效果达到预期。仔细思考一下,可能是这样的:编译器错误的以为i是不会变的,因此将代码优化为类似这样的:

void main(void) {
  LED = 0;
  char i = 0;
  while (1) {
    delay();
    // select_38 宏被展开为:
    select_38_0; // 这个宏导致始终是第一个灯亮
    break; // 这个break 导致了后面的代码不可达
    i = (i + 1) % 8;
  }
}

初步验证
我有一个不太严谨的方法验证一下这个猜想,即我们可以用volatile关键字来修饰下i,清楚的告诉编译器i是会变的。这样如果原程序通过编译,那我的猜测就有可能是正确的。

  LED = 0;
  volatile char i = 0;
  while (1) {
    delay();
    select_38(i);
    i = (i + 1) % 8;
  }

这样更改后果然就通过编译了。

我认为这里至少有两个问题:

  1. 错误的认为i是不会变的
  2. 错误的展开了select_38宏: 就所i真的是不变的,展开后的宏不应该包含那个break语句。那个break语句本意是break对应的case,留下后错误的break了这里的while循环.

通过模拟软件,让我们零成本玩玩单片机。

第1步,安装模拟器。

yay -S simulide

yay 是Arch AUR 的包管理器,你应该需要根据你使用的系统来安装对应的软件。

Simulide

像图中那样连接组建,点击红色的开关就可以开始模拟了。运行起来后,可以看见一个发黄光的二极管。(因为P1.0 默认会输出高电平)

第2步,安装编译器

yay -S sdcc

详见:https://sdcc.sourceforge.net/

第3步,编写LED闪烁代码

以下为led.c的内容

#include <8052.h>
void delay() {
  int i, j;
  for (i = 0; i < 0xff; i++) {
    for (j = 0; j < 0xff; j++)
      ;
  }
}
void main() {
   while (1) {
     P1 = 0xff;
     delay();
     P1 = 0;
     delay();
   }
}

这里P1=0xff 将让P1.0 ~ P1.7 这八个pin输出为高电平,P1=0 将让这些pin输出低电平。通过两重空循环在这两个操作之间引入一些延时而达到一种一闪一闪的效果。

第4步,编译

sdcc led.c

这个命令会生成一堆文件:

files

然后生成hex文件

packihx led.ihx >led.hex

第5步,上载到模拟器中

Simulide

这样就可以看见这个黄色的LED在闪速了。

typescript 4.7 显式Variance注解

什么是Variance

variance 是用来描述泛型类型之间的关系的。

interface Animal {
    animalStuff: any
}
interface Dog extends Animal {
    dogStuff:any
}

type A<T> = ...

什么是子类型?同常我们讲一个类型是另一个类型的子类型。我们将的是可赋值性。既需要父类型的地方可以安全的使用子类型代替。比如这里,Dog 是Aniaml的子类型,在需要Animal的地方,可以使用Dog。从集合的角度来看,子类型是父类型的子集。

对于泛型类型来说,一个类型是不是另一个类型的子类型,可能就不能简单的是或者否来回答。分以下三种情况,每种情况有一个术语来称呼:

若A 也 是 A 的子类型,这叫协变covariant,Dog ⊆ Animal, A ⊆ A

若A 是A 的子类型,这叫逆变contravariant,Dog ⊆ Animal, A ⊇ A

若A 不是A 的子类型,A 也不是A的子类型,这就叫不变invariant,

协变

interface Animal {
    animalStuff: any
}
interface Dog extends Animal {
    dogStuff:any
}
type A<T> = {
    value: T
}
type B<T> = ()=>T

declare let a:A<Animal>
declare let b:A<Dog>

A是协变的。因为A 可以赋值给A

/*
b 可以赋值个 a, 因为 b 为 { value: Dog } a 为 { value: Animal },
我们可以把Dog 当做Animal 使用,A是协变的。
*/
a=b
/*
反过来不行,因为不能把Aniaml 当做 Dog来使用。
*/
b=a;// type error

同样B也是协变的:

declare let c: B<Animal>
declare let d: B<Dog>
/*
c 是一个返回Animal的函数,因此在使用c的时候我们只会使用Animal的属性,
d 是一个返回Dog的函数,如果把d 赋值给c,则会把d返回的Dog 当Animal来使用
从类型层面来说,这自然是安全的。
*/
c = d;
/*
反过来,如果把Animal 当做Dog来使用,则类型不安全,因为有些属性是Dog 有而Animal没有的。
所以,c不可赋值给d, 类型B是协变的。
*/
d = c;// error

typescript 4.7 使用out 来显示表示某个泛型是协变的。

interface Animal {
    animalStuff: any
}
interface Dog extends Animal {
    dogStuff:any
}
type A<out T> = {
    value: T
}

逆变

interface Animal {
    animalStuff: any
}
interface Dog extends Animal {
    dogStuff:string
}
interface Cat extends Animal {
    catStuff:unknown
}

type A<T> = (v:T) =>void
declare let wantAnimal: A<Animal>
declare let wantDog: A<Dog>
declare let aCat:Cat

现在的A的变性是协变、逆变还是不变呢?A是逆变的,因为A 不是A的子类型。那么为什么A 不是A的子类型呢?下列赋值是不全的:

wantAnimal = wantDog

因为wantDog 接受一个更小范围的值,而wantAnimal接受一个更大范围的值。例如wantAniml(aCat)是可以的,因为Cat 是Animal的子类型。而wantDog(cat)是不行的,因为Cat 是Cat ,Dog 是Dog,它们都是派生自Animal的。假如wantDog可以赋值给wantAnimal,那么wantAnimal(aCat)再运行时就可能出错(如果wantDog访问了dog独有的属性)。

function dog(d:Dog) {
    console.log(d.dogStuff.toUpperCase());
}
wantAnimal = dog as any;
wantAnimal(aCat) // Oops,call toUpperCase on undefined

逆变用in 关键字来描述。

interface Animal {
    animalStuff: any
}
interface Dog extends Animal {
    dogStuff:any
}

type A<in T> = (v:T) =>void

不变

不变就是把前面两种情况组合起来

interface Animal {
    animalStuff: any
}
interface Dog extends Animal {
    dogStuff:any
}

type A<T> = {
    get:()=> T,
    set:(v:T) =>void
}
declare let a:A<Animal>
declare let b:A<Dog>
/*
如个A只有get,那么A是协变的,如果A只有set,那么A是逆变的,可惜A都有,A就是不变的了
*/
a=b;// error, set不兼容
b=a;// error, get不兼容

同时使用inout来标识不变

interface Animal {
    animalStuff: any
}
interface Dog extends Animal {
    dogStuff:any
}

type A<in out T> = {
    get:()=> T,
    set:(v:T) =>void
}
type B<out T,in U> = {
    get:()=> T,
    set:(v:U) =>void
}
declare let a:A<Animal>
declare let b:A<Dog>
/*
如个A只有get,那么A是协变的,如果A只有set,那么A是逆变的,可惜A都有,A就是不变的了
*/
a=b;// error, set不兼容
b=a;// error, get不兼容

为什么要引入这两个关键字

in 表示这个类型参数是用做”输入“的,out表示这个类型参数是用做输出的。对于较为复杂的类型,如果显示的标出类型参数的variance, 就不需要使用者思考这个类型参数的variance了。另外,typescript 自己会去计算每个类型参数的variance,对于复杂的类型,这个计算开销是比较大的。

简介

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

Hello World

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

function identity(arg: number): number {
    return arg;
}

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

function identity(arg: any): any {
    return arg;
}

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

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

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

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

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

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

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

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

let output = identity("myString")

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

使用泛型类型变量

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

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

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

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

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

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

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

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

泛形类型

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

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <T>(arg: T) => T = identity;

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

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <U>(arg: U) => U = identity;

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

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: {<T>(arg: T): T} = identity;

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

interface GenericIdentityFn {
    <T>(arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn = identity;

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

interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

注意,我们的例子变的有点不同了。

泛型类

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

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; };
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属性,所以它会给出一个警告。

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关键字来说明我们的限制。

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;
}

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

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

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

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

在类型参数中使用限定

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

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,
当我们在工厂函数中使用泛型的时候,在其构造函数中引用类类型是有必要的。例如:

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

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

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!

简介

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

函数

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

// Named function
function add(x, y) {
    return x + y;
}

// Anonymous function
let myAdd = function(x, y) { return x + y; };

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

let z = 100;

function addToZ(x, y) {
    return x + y + z;
}

函数类型

添加类型

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

function add(x: number, y: number): number {
    return x + y;
}

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

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

写函数类型

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

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

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

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

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

推断类型

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

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

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中可以通过在参数名末尾添加?来使用这一功能。比如,如果第二个参数是可选的:

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),那么该参数的值就是预设值。这被叫做默认参数.

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

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

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

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

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

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中,你可以把所有的参数收集到同一个变量中:

function buildName(firstName: string, ...restOfName: string[]) {
    return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

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

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是可用的。这是一个强大的、灵活的特性,但其代价是总是需要知道函数正在执行的上下文是什么。这是十分具有迷惑性的,特别是在返回一个函数或以一个函数为参数的时候。
例如:

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返回的函数中的thiswindow而非deck对象。这是因为我们以顶层非方法的语法的方式在调用cardPicker(原注:在严格模式下,this会是undefined)。
我们可以通过确保function 绑定到正确的this的方式修正这个问题。这种情况下,无论这个函数多晚被调用,它总可以访问到deck对象。为了做到这点,我们可使用ES6的箭头函数,箭头函数会在函数被创建的时候捕获this

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]中的thisany类型的。

this 参数

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

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

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

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对象上被调用,这表明thisDeck类型的而不是any,所以--noImplicitThis不会导致问题。

回调中的this参数。

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

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中函数根据传入的参数返回不同的对象是很常见的。

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的参数和返回值。

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函数只有两个重载,一个是以对象为参数的另一个是以数字为参数的。以其它类型的参数来调用将得到一个编译时错误。

简介

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

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

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

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关键字来创建一个子类。这里你可以看见HoresSnake继承了Animal类,并能访问基类中的属性。
包合构造函数的子类必须调用通过super()来基类中的构造函数。

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

Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.

public,private 以及protected 修饰器

public

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

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,将不可以在其包含它的类外面访问了。例如:

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

new Animal("Cat").name; // Error: 'name' is private;

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

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

理解protected

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

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修饰,这表明这个类不能从外部实例化,但是可被继承。例如:

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关键字来表示一个属性是只读的。只读属性只能在其声明的地方或构造器中被初始化。

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类的构造函数中初始化了这个属性。
这其实是一种常见的模式,参数属性可以简化这个过程,让你在同一个地方创建并初始化一个成员。

class Octopus {
    readonly numberOfLegs: number = 8;
    constructor(readonly name: string) {
    }
}

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

访问器

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

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可被获取

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.来访问静态成员。

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关键字来定义抽象类和抽象方法。

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log("roaming the earth...");
    }
}

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

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中声明了一个类,你实际上同时声明了很多东西。首先是类:

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

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的时候,就会调用这个构造函数并得到一个实例。构造函数上也包含了类的静态成员。另一个思考类的方式是其可分为实例侧静态侧
修改一下前面的例子来演示其中的不同

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的实例。

类用作接口

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

class Point {
    x: number;
    y: number;
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

Introduction

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

第一个接口

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

function printLabel(labelledObj: { label: string }) {
    console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

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

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”(注:即把所有的选项放在一个对象里面)的模式的时候可选属性是很常用的。

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将给出一个错误消息。

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关键字来说明这点。

interface Point {
    readonly x: number;
    readonly y: number;
}

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

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

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

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变量。
除非你使用类型断言:

a=ro as number[]

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

多余属性检查

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

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。***当一个对象被赋值 给其它变量,或通过参数传递的时候,对象会被特殊对待,经过所谓的”多余属性检查“**。如果该对象含有目标类型所没有的属性,就会报错:

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

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

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

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

interface SquareConfig {
    color?: string;
    width?: number;
    [propName: string]: any;
}

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

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

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

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

函数类型

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

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

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

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

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

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

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

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

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

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

可索引的类型

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

interface StringArray {
    [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

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

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

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中没有这个限制,那么就会有下面演示的问题

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

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

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
}

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

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

类类型

实现一个接口

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

interface ClockInterface {
    currentTime: Date;
}

class Clock implements ClockInterface {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

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

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)。如果你创建了一个拥有构造函数的签名的接口,并试图用一个类来实现这个接口,那你会得到一个错误:

interface ClockConstructor {
    new (hour: number, minute: number);
}

class Clock implements ClockConstructor {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

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

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)是可以的。

扩展接口

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

interface Shape {
    color: string;
}

interface Square extends Shape {
    sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;

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

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的动态性和灵活性,你也许会遇到一个对象,它是好几种类型混合的结果。
例如,一个东西既是一个有属性的对象又是一个函数。

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库的时候,你也许需要这一特性来完全描述对象的类型。

接口扩展类

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

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的子类才有这些同处声明的私有成员,这是私有成员类型兼容的必要条件。