TS相关
TS相关
概述
微软推出的js超集,目的是简化js的编写。很多框架底层是用ts写的。
优点主要在于“Type”,是面向对象的,它也支持指定类型。另外,js只能在运行时报错,但ts可以在编译期发现错误。
安装
全局安装:
npm install -g typescript@latest
编译并运行命令:
tsc index.ts
node index.js
监听文件修改并自动编译:
tsc -w index.ts
基础类型
数组
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];
元组
let x: [string, number];
x = ['hello', 10];
访问已知索引的元素:
console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
当访问一个越界的元素,会使用联合类型替代:
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型
console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString
x[6] = true; // Error, 布尔不是(string | number)类型
枚举
enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];
console.log(colorName); // 显示'Green'因为上面代码里它的值是2(指定了从1开始编号)
Any
有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么可以使用 any
类型来标记这些变量:
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // ok
any
允许你在编译时可选择地包含或移除类型检查,比Object
还强。Object
类型的变量只是允许你给它赋任意值 - 但是却不能够在它上面调用任意的方法,即便它真的有这些方法:
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'.
也可以指定any
类型的数组:
let list: any[] = [1, true, "free"];
list[1] = 100;
void
void
类型像是与any
类型相反,它表示没有任何类型,一般被用来当做返回值类型。 一个void
类型的变量只能被赋予undefined
和null
:
function warnUser(): void {
console.log("This is my warning message");
}
let unusable: void = undefined;
Null & Undefined
undefined
和null
两者各自有自己的类型分别叫做undefined
和null
:
// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;
除此以外这两个类型的变量不能再被赋予其它任何值。
默认情况下null
和undefined
是所有类型的子类型。 就是说你可以把 null
和undefined
赋值给number
等类型的变量。如果指定了--strictNullChecks
标记,null
和undefined
只能赋值给void
和它们各自。 这能避免很多常见的问题。
Never
never
类型表示的是那些永不存在的值的类型。 例如, never
类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。如果一个变量被永不为真的类型保护所约束时,它的类型也是Never:
function error(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {
}
}
never
类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never
的子类型或可以赋值给never
类型(除了never
本身之外)。 即使 any
也不可以赋值给never
。
类型断言
类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你已经进行了必须的检查:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
// 或
let strLength: number = (someValue as string).length;
函数
类型限定
可以为参数和返回值限定类型:
function add(x: number, y: number): number {
return x + y;
}
let myAdd = function(x: number, y: number): number { return x + y; };
如果想要把函数赋值给别人,需要写出完整的函数类型:
let myAdd: (x: number, y: number) => number = function(x: number, y: number): number { return x + y; };
如果在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript编译器会自动识别出类型:
let myAdd: (baseValue: number, increment: number) => number = function(x, y) { return x + y; };
可选参数和默认参数
TypeScript里的每个函数参数都是必须的,编译器检查用户是否为每个参数都传入了值。 编译器还会假设只有这些参数会被传递进函数。也就是说,参数有几个就得传几个,不能多也不能少。
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");
可选参数必须跟在必须参数后面。 如果上例我们想让first name是可选的,那么就必须调整它们的位置,把first name放在后面:
function buildName(lastName: string, firstName?: string) {...}
可以为函数参数设置默认值,这个默认参数的位置如果被传进undefined,也会被当做没传处理(使用默认值):
function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // "Bob Smith"
let result2 = buildName("Bob", undefined); // "Bob Smith"
let result3 = buildName("Bob", "Adams", "Sr."); // error
带默认值的参数不需要放在必须参数的后面。 如果带默认值的参数出现在必须参数前面,用户必须明确的传入 undefined
值来获得默认值:
function buildName(firstName = "Will", lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // error, 参数从左边匹配,这个Bob会传给firstName
let result4 = buildName(undefined, "Adams"); // okay and returns "Will Adams"
剩余参数
可以把所有参数收集到一个变量里:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
// 如果用了剩余,参数想传多少都行:
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie", ...);
重载
ts支持像c++一样的函数重载:
function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
高级类型
交叉类型
交叉类型是通过&符号将多个类型合并为一个类型。 它包含了所需的所有类型的特性。 例如, Person & Serializable & Loggable
同时是 Person
和 Serializable
和 Loggable
。 就是说这个类型的对象同时拥有了这三种类型的成员。
type Class = ClassA & ClassB
let info:Class = {
name:'zhagsan',
age:18,
phone:1573875555
}
当然,并不是什么类型都能合并的,比如string&number
,这肯定是错误的,因为不可能有既满足字符串又满足数字类型。另外,如果合并的接口类型中具有同名属性,且类型不同,则合并后类型为never
:
interface X{
q:number,
w:string
}
interface Y{
q:boolean,
r:string,
}
type XY = X&Y // XY是never类型
联合类型
联合类型通过|
符号(其实可以理解为“或”操作)连接多个类型从而生成新的类型。它主要是取多个类型的交集,即多个类型共有的类型才是联合类型最终的类型。联合类型可以是多个类型其中一个,可做选择,比如:string | number
,它的取值可以是string
类型也可以是number
类型:
let a:string|number|boolean;
a = 's';
a = 1;
a= false;
如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员:
interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(): Fish | Bird {
// ...
}
let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim(); // errors
类型保护机制
有时需要判断,一个对象的类型是否是我期待的,如果是再调用它上面的方法,可以用类型断言实现:
let pet = getSmallPet();
if ((<Fish>pet).swim) {
(<Fish>pet).swim();
}
else {
(<Bird>pet).fly();
}
TypeScript提供的类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个类型谓词:
function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}
每当使用一些变量调用 isFish
时,TypeScript会将变量坍缩为那个具体的类型:
if (isFish(pet)) {
pet.swim();
}
else {
pet.fly();
}
i
TypeScript不仅知道在 if
分支里 pet
是 Fish
类型; 它还清楚在 else
分支里,一定不是 Fish
类型,一定是 Bird
类型。这部分可以对比量子物理中的状态叠加来理解,当观测后,对象的类型会坍缩到一个类型,同时自然也就知道它此时不是另一个类型。
类型别名
类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型:
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
配合泛型可以创建出链表节点类型(自己包含自己):
type LinkedList<T> = T & { next: LinkedList<T> };
interface Person {
name: string;
}
var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;
但是类型别名不能出现在声明右侧:
type Yikes = Array<Yikes>; // error
字面量类型
字面量类型允许你指定某种类型必须的固定值:
type Easing = "ease-in" | "ease-out" | "ease-in-out";
function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
// ...
}
接口
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
LabelledValue
接口就好比一个名字,用来描述上面例子里的要求。 它代表了有一个 label
属性且类型为string
的对象。在这里并不能像在其它语言里一样,说传给 printLabel
的对象实现了这个接口。ts只会去关注值的外形。上面的代码等同于:
function printLabel(labelledObj: { label: string }) {
console.log(labelledObj.label);
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
可选属性
接口里的属性不全都是必需的,类似函数的可选参数,加上问号就行:
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): {color: string; area: number} {
// ...
}
let mySquare = createSquare({color: "black"});
只读属性
一些对象属性只能在对象刚刚创建的时候修改其值。 可以在属性名前用 readonly
来指定只读属性:
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error
函数类型
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}
参数名可以不一样,但是参数的类型必须与接口兼容。
可索引类型
可索引类型具有一个索引签名,它描述了对象索引的类型,还有相应的索引返回值类型:
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
本例中StringArray
定义了索引为number,值是string的索引类型,就是一个string数组。
TypeScript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number
来索引时,JavaScript会将它转换成string
然后再去索引对象。 也就是说用 100
(一个number
)去索引等同于使用"100"
(一个string
)去索引,因此两者需要保持一致。
类类型
这部分与Java一样,类接口用来明确地强制一个类去符合某种契约。
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
接口只描述类的公共部分,它不会帮你检查类是否具有某些私有成员。
类的静态与实例部分详解
类是具有两个类型的:静态部分的类型和实例的类型。
构造器签名
构造器签名是 TypeScript 中用于描述类的构造函数类型的一种方式。它定义了构造函数的参数类型和返回的实例类型。写法是在interface里写一个名为new的函数。之后就可以直接new 接口实例(参数),返回值是要构造的类共同实现的接口。
当用构造器签名去定义一个接口并试图定义一个类去实现这个接口时:
interface ClockConstructor {
new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
会报错,因为类不能直接 "实现" 一个构造签名接口,因为 implements
检查的是实例端(prototype
),而构造签名是静态端(constructor
)。这就会导致implement在检查Clock是否实现了ClockConstructor的所有成员时找不到new方法(因为它只管看实例端,它不知道new是隐含的)。
正确的做法是:
// 实例接口
interface ClockInterface {
tick(): void;
}
// 构造函数接口
interface ClockConstructor {
new(h: number, m: number): ClockInterface;
}
// 类只实现实例接口
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) {}
tick() { console.log("beep beep"); }
}
// 通过工厂函数处理构造函数
function createClock(ctor: ClockConstructor, h: number, m: number) {
return new ctor(h, m);
}
// 使用
const digital = createClock(DigitalClock, 12, 17);
包含new的构造函数接口只在工厂函数里被用到,类只实现实例接口。
接口继承
与Java写法一样:
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
ts中接口可以继承一个类,当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement):
模块
导出
任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加export
关键字来导出。一个模块包裹多个模块时,把他们导出的内容联合在一起通过语法:export * from "module"
导出。
重新导出功能允许你从一个模块中导出另一个模块的内容:
// components/Button.tsx
export const Button = () => {
return <button>Click me</button>;
};
// components/index.ts
export { Button } from './Button';
// 也可以写成: export * from './Button';
默认导出
每个模块都可以有一个default
导出。 默认导出使用 default
关键字标记;并且一个模块只能够有一个default
导出。 需要使用一种特殊的导入形式来导入 default
导出。
// JQuery.ts
declare let $: JQuery;
export default $;
// App.ts
import $ from "JQuery";
$("button.continue").html( "Next Step..." );
在导入默认导出时,可以随意取个别名:
// ZipCodeValidator.ts
export default class ZipCodeValidator {
static numberRegexp = /^[0-9]+$/;
isAcceptable(s: string) {
return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
}
}
// Test.ts
import validator from "./ZipCodeValidator";
let myValidator = new validator();
也可以仅导出一个值,也可以在import时随意取名:
// OneTwoThree.ts
export default "123";
// Test.ts
import num from "./OneTwoThree";
export =
和 import = require()
用于与 CommonJS/AMD 模块系统的互操作性。这些语法在 TypeScript 中用于更精确地模拟 Node.js 的模块系统行为。export =
语法用于导出一个模块的单一导出,类似于 CommonJS 的 module.exports
。它是 TypeScript 对 CommonJS/AMD 模块导出方式的直接模拟。
class Calculator {
add(a: number, b: number): number {
return a + b;
}
}
export = Calculator;
导入export=
时要import = require()
:
import Calculator = require('./math');
const calc = new Calculator();
console.log(calc.add(1, 2));
命名空间
基本概念和用法
命名空间其实就是规定了命名重复的范围,两个不同的命名空间中名字可以重复,用的时候指定好在哪个空间就行。
下面的例子里,把所有与验证器相关的类型都放到一个叫做Validation
的命名空间里。 因为我们想让这些接口和类在命名空间之外也是可访问的,所以需要使用 export
。 相反的,变量 lettersRegexp
和numberRegexp
是实现的细节,不需要导出,因此它们在命名空间外是不能访问的。 在文件末尾的测试代码里,由于是在命名空间之外访问,因此需要限定类型的名称,比如 Validation.LettersOnlyValidator
:
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^[0-9]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
for (let s of strings) {
for (let name in validators) {
console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
}
}
多文件分割命名空间
把Validation
命名空间分割成多个文件。 尽管是不同的文件,它们仍是同一个命名空间,并且在使用的时候就如同它们在一个文件中定义的一样。 因为不同文件之间存在依赖关系,所以我们加入了引用标签来告诉编译器文件之间的关联:
// Validation.ts
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
}
// LettersOnlyValidator.ts
/// <reference path="Validation.ts" />
namespace Validation {
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
}
// ZipCodeValidator.ts
/// <reference path="Validation.ts" />
namespace Validation {
const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}
最后使用时要加上对所有含有命名空间的文件的引用标签:
// Test.ts
/// <reference path="Validation.ts" />
/// <reference path="LettersOnlyValidator.ts" />
/// <reference path="ZipCodeValidator.ts" />
let strings = ["Hello", "98052", "101"];
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
for (let s of strings) {
for (let name in validators) {
console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
}
}
注意,如果分了多个文件,编译时要加上--outFile
参数,编译器会根据源码里的引用标签自动地对输出进行排序:
tsc --outFile sample.js Test.ts
使用别名
一种简化命名空间操作的方法是使用import q = x.y.z
给常用的对象起一个短的名字:
namespace Shapes {
export namespace Polygons {
export class Triangle { }
export class Square { }
}
}
import polygons = Shapes.Polygons;
let sq = new polygons.Square(); // Same as "new Shapes.Polygons.Square()"
不要与用来加载模块的 import x = require('name')
语法弄混了,这里的语法是为指定的符号创建一个别名。 你可以用这种方法为任意标识符创建别名,也包括导入的模块中的对象。
外部命名空间
为了描述不是用TypeScript编写的类库的类型,我们需要声明类库导出的API,通常在 .d.ts
里写这些声明。 由于大部分程序库只提供少数的顶级对象,命名空间是用来表示它们的一个好办法。这里的声明真的只是声明,不包含任何功能,类似c++里的.h
文件。
程序库D3在全局对象d3
里定义它的功能。 因为这个库通过一个 <script>
标签加载(不是通过模块加载器),它的声明文件使用内部模块来定义它的类型。 为了让TypeScript编译器识别它的类型,我们使用外部命名空间声明:
// D3.d.ts
declare namespace D3 {
export interface Selectors {
select: {
(selector: string): Selection;
(element: EventTarget): Selection;
};
}
export interface Event {
x: number;
y: number;
}
export interface Base extends Selectors {
event: Event;
}
}
declare var d3: D3.Base;
import的流程
编译器首先尝试去查找相应路径下的.ts
,.tsx
再或者.d.ts
。 如果这些文件都找不到,编译器会查找外部模块声明。
/// <reference path="myModules.d.ts" />
import * as m from "SomeModule";
这段代码中,为了防止编译器找不到SomeModule
,用引用标签指定了外来模块的位置。它指向的文件里包含SomeModule
的声明:
// In a .d.ts file or .ts file that is not a module:
declare module "SomeModule" {
export function fn(): string;
}