TypeScript 是 JavaScript 的类型的超集,其增长了代码的可读性和可维护性,它会进行静态检查,若是发现有错误,编译的时候就会报错(虽然会报错了,但仍是会生成编译结果)。javascript
本文大量参考官方文档,如需深刻学习请前往官方查看,本文目的只是一些内容的精简记录,方便查询(本文是在官方文档基础上加上谷歌翻译、zhongsp的typescript使用手册、TypeScript 入门教程、黯羽轻扬的typescript笔记和本身的理解,因此文字方面有些是我本身的语言,不过代码方面均基于3.9的环境运行过)java
基于版本3.9,会不按期更新node
npm install -g typescript // typescript
npm install -g ts-node // 在node环境下直接编译运行ts文件
...
/// 经过 tsc -V 查看安装是否成功
复制代码
tsc --init // 任意目录初始化tsconfig
复制代码
/// tsconfig.json
{
"compilerOptions": {
...
// "outDir": "./", // 把此处的输出路径打开
},
"files": ["index.ts"] // 指定编译文件
}
复制代码
/// 编译,因为运行的是默认配置,能够简写为 tsc
tsc -p ./tsconfig.json
...
/// 在node环境直接运行
ts-node index.ts
复制代码
typescript 兼容 javascript 的数据类型,javascript 有下面七种数据类型:react
let isDone: boolean = false;
复制代码
布尔值指的是 boolean
,而非 Boolean 这个包装对象jquery
let createdByNewBoolean: boolean = new Boolean(1);
// 不能将类型“Boolean”分配给类型“boolean”。
“boolean”是基元,但“Boolean”是包装器对象。如可能首选使用“boolean”。ts(2322)
...
let createdByNewBoolean: Boolean = new Boolean(1); // 这是能够的
let createdByBoolean: boolean = Boolean(1); // 这也能够
复制代码
和JavaScript同样,TypeScript里的全部数字都是浮点数。 这些浮点数的类型是 numbergit
let decLiteral: number = 6; // 10进制
let hexLiteral: number = 0xf00d; // 16进制
let binaryLiteral: number = 0b1010; // 2进制
let octalLiteral: number = 0o744; // 8进制
复制代码
使用 string表示文本数据类型。 和 JavaScript 同样,可使用双引号( ")或单引号(')表示字符串es6
let name: string = "bob";
复制代码
支持模版字符串github
let name: string = 'Gene';
let sentence: string = `Hello, my name is ${ name }.`
复制代码
let list: number[] = [1, 2, 3];
复制代码
let list: Array<number> = [1, 2, 3];
复制代码
元组类型容许表示一个已知元素数量和类型的数组,各元素的类型没必要相同ajax
// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
...
x = [10, 'hello']; // Error
复制代码
当访问一个已知索引的元素,会获得正确的类型typescript
console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
复制代码
访问已知索引外的元素会失败,并显示如下错误
x[3] = 'world'; // Error Property '3' does not exist on type '[string, number] 复制代码
enum 枚举是一种为数字值集,赋予更友好名称的方法
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
复制代码
默认状况下,从0开始为元素编号。 你也能够手动的指定成员的数值,或者所有都采用手动赋值
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green; // c为2
复制代码
这有一个比较有用的场景
enum fetchStatus {
success = '000000',
noLogin = '910001',
timeOut = '910002'
}
if (data.code === fetchStatus.success) {...}
/// 这里能够假定登录接口返回的 code 码有多种含义,经过枚举方式的定义,能增长代码的可读性
复制代码
为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,好比来自用户输入或第三方代码库。
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
复制代码
若是只知道部分数据类型时,也可使用any类型
let arr: Array<any> = [2, 'ss']
复制代码
在 any
类型上访问任何属性都是容许的
let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);
复制代码
也容许调用任何方法
let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');
复制代码
声明一个变量为任意值以后,对它的任何操做,返回的内容的类型都是任意值(此处能够类比成一个js变量,想作什么都想,在编译阶段都不会报错,只有在运行时才知对应信息存不存在)
void 类型像是与 any 类型相反,它表示没有任何类型。当一个函数没有返回值时,返回值类型能够是 void
function warnUser(): void {
console.log("This is my warning message");
}
复制代码
声明为void类型的变量只能为其赋值 undefined
和 null
(null
须要修改 tsconfig.json
中的 strictNullChecks
打开配置)
let unusable: void = undefined;
复制代码
undefined
和 null
二者各自有本身的类型分别叫作 undefined
和 null
,默认状况下 null
和 undefined
是全部类型的子类型
let u: undefined = undefined;
let n: null = null;
复制代码
当指定了--strictNullChecks
标记,null和undefined只能赋值给void和它们各自
never 类型表示的是那些永不存在的值的类型,好比那些老是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型
function error(message: string): never {
throw new Error(message);
}
复制代码
另一个用处就是在处理类型收窄, 避免出现新增了联合类型没有对应的实现
/// https://www.zhihu.com/question/354601204 参考尤雨溪的回答
interface Foo {
type: 'foo'
}
interface Bar {
type: 'bar'
}
type All = Foo | Bar
function handleValue(val: All) {
switch (val.type) {
case 'foo':
// 这里 val 被收窄为 Foo
break
case 'bar':
// val 在这里是 Bar
break
default:
// val 在这里是 never
const exhaustiveCheck: never = val
break
}
}
/// 如上All 是一个联合类型,handleValue 中已经把 All 在定义时,全部的可能值都作了处理,最后在 default 中设置为 never,若是另一我的改了 All 的类型,那么就必须在 handleValue 中进行对应处理,从而达到保护的做用
复制代码
object表示非原始类型,也就是除number,string,boolean,symbol,null或undefined以外的类型。
类型断言能够用来手动指定一个值的类型,在后续的联合类型的介绍中会再介绍
/// 下面是官网的例子,不过考虑以前 any 的特性,这里其实不用断言,也同样能正常使用
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
...
/// 有时会不免会为 window 添加某些属性,直接这么写 ts 会报错,告诉不存在这个属性
window.foo = 1;
...
(<any>window).foo = 1; // <>写法
(window as any).foo = 1; // as写法
复制代码
若是没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型
let info = 'info';
info = 7; // Error 此处会推导 info 是 string 类型,因此不能把 number 赋值给 info
...
// 若是定义的时候没有赋值,无论以后有没有赋值,都会被推断成 any 类型而彻底不被类型检查
let info;
info = 'info';
info = 7; // 两次赋值正确
复制代码
当须要从几个表达式中,推断类型时。会使用这些表达式的类型,推断出一个最合适的通用类型
let zoo = [new Rhino(), new Elephant(), new Snake()];
// 此时 ``zoo`` 会被推到为联合数组类型 ``(Rhino | Elephant | Snake)[]``
...
let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];
// 此处 ``zoo`` 有明确的指示, 因此为 ``Animal[]`` 类型
复制代码
参考知乎文章
const s: (v: string) => void = v => {};
/// s 是一个函数类型,v 并无明确的指明类型,但此处 ts 会推导其为 string 类型
...
function f (a: (v: string) => void = vv => {}) {}
/// 函数的参数类型是 (v: string) => void,默认值是 vv => {},因此推导出 vv 是 string 类型
...
const s1: (a: {foo: (v: string) => void}) => void = ({foo = vv => {}}) => {}
/// s1 的类型是 (a: {foo: (v: string) => void}) => void ,对应赋值为 ({foo = vv => {}}) => {}
/// 第二层:赋值的函数 a 的类型是 {foo: (v: string) => void} 对应赋值为 {foo = vv => {}}
/// 第三层,这里的 foo 对应的类型 (v: string) => void 就是 vv => {},因此,推导出 vv 仍是 string
...
class C1 {
foo: (v: string) => void = vv => {}
}
/// 这里 foo 的类型是 (v: string) => void 对应赋值为 vv => {},vv 仍是 string
复制代码
总的来讲这种推导规则稍微的有些绕,分析使用状况,能推导出对应值的属性,这个更可能是实现细节,通常使用时,要么写明规则,要么彻底不写重回 js 模式
let input = [1, 2];
let [first, second] = input;
复制代码
能够在数组里使用 ... 语法建立剩余变量
let [first, ...rest] = [1, 2, 3, 4];
复制代码
元祖是一种特殊数组,因此数组的解析规则使用元组
let tuple: [number, string, boolean] = [7, "hello", true];
let [a, b, c] = tuple; // a: number, b: string, c: boolean
复制代码
let o = {
a: "foo",
b: 12,
c: "bar"
};
let { a, b } = o;
复制代码
let { a: newName1, b: newName2 } = o;
复制代码
若是要指定类型能够这么写
let {a, b}: {a: string, b: number} = o;
复制代码
function add(input: {a:number, b:number, c?:number}) {
let {a, b, c = 100} = input;
return a + b + c;
}
复制代码
ts 会比 js 多了些类型检查,容易形成在设置解构时看起来很长,不容易理解
/// 分清是给参数赋默认值,仍是给解构赋默认值
/// 下面是给参数赋默认值
function add(input: {a:number, b:number, c?:number} = {a:0, b:0, c:100}) {
let {a, b, c = 100} = input;
return a + b + c;
}
add({a: 3, b: 2}); // [3, 0]
add();
...
/// 下面是给解构赋予默认值
function add1({a = 0, b = 0, c = 100}: {a?:number, b?:number, c?:number}) {
return a + b + c;
}
add1({a: 3, b: 2}); // [3, 0]
add1({}); // [0, 0]
复制代码
展开进行的是浅拷贝
数组的规则简单,直接展开变成一个新的数组
let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];
复制代码
对象展开时,因为从左往右进行,因此后面的属性值会覆盖前面的同名属性
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };
复制代码
TypeScript 的核心原则之一是对值所具备的结构进行类型检查。 它有时被称作**“鸭式辨型法”或“结构性子类型化”**。 在 TypeScript 里,接口的做用就是为这些类型命名和为你的代码或第三方代码定义契约
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
复制代码
interface LabelledValue
就是定义的接口,表明了有一个 label 属性且类型为 string 的对象
interface SquareConfig {
color?: string;
width?: number;
}
复制代码
接口下的属性值,可存在也可不存在,除可选属性外没法赋值
interface Point {
readonly x: number;
readonly y: number;
}
...
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
复制代码
ReadonlyArray<T>
类型,能够确保数组建立后不能被修改let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
复制代码
a = ro; // error!
复制代码
a = ro as number[];
复制代码
做为变量使用的话使用 const,做为属性使用的话用 readonly
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config:SquareConfig) {}
createSquare({width: 100})
/// 这里咱们传的属性只知足其中一条可选属性,这是没问题的,由于color是可选的
...
createSquare({ colour: "red", width: 100 });
/// 可是若是咱们传入一个设置以外的参数值,就会报错
复制代码
由于编译器此时会进行额外的属性检查,colour 是额外的属性值,接口中并无针对这个值作类型设置
解决这个问题有两种方法:
let mySquare = createSquare({ colour: "red", width: 100 } as SquareConfig);
复制代码
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
复制代码
这样就表示只关注 color
和 width
的值,其余值类型是 any,不过这种方式是不建议使用的,若是多余的参数是必须的话,那应该对其定义
接口除了描述带有属性的普通对象外,也能够描述函数类型。
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function (source: string, subString: string) {
let result = source.search(subString);
return result > -1;
};
复制代码
对于函数类型的类型检查来讲,函数的参数名不须要与接口里定义的名字相匹配。
mySearch = function(src: string, sub: string): boolean {
复制代码
可索引类型具备一个 索引签名
,它描述了对象索引的类型,还有相应的索引返回值类型
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
复制代码
上例定义了 StringArray 接口,它具备索引签名。 这个索引签名表示了当用 number 去索引StringArray 时会获得 string 类型的返回值
TypeScript支持两种索引签名:字符串和数字,能够同时使用两种索引,可是数字索引的返回值必须是字符串索引返回值的子类型。这是由于当使用 number 来索引时,JavaScript会将它转换成 string而后再去索引对象。 也就是说用 100 去索引等同于使用 "100"去索引,所以二者须要保持一致。
/// 这里能够类比下 js 中的代码
let ad = ['info', 'foo'];
ad[0];
ad['0']; // 索引值是数字或者字符访问均可以
...
class Animal {
constructor(public name:string) {}
}
class Dog extends Animal {
constructor(public breed:string, public name:string) {
super(name);
}
}
interface NotOkay {
[x: number]: Animal; // error
[x: string]: Dog;
}
复制代码
此时会报错:数字索引类型“Animal”不能赋给字符串索引类型“Dog”,缘由就是说,当索引key 值使用不一样索引签名类型(分别为 number、string),对应的索引返回值类型不一样(分别为Animal、Dog)
此时若是只使用
number
或string
做为索引key
值都不会报错
interface NotOkay {
[x: number]: Animal;
}
复制代码
设置索引时,与给定的变量名无关
interface NotOkay {
[x: number]: Animal;
[y: string]: Dog; // Error,x y 是两个变量名,但实际上它们表明的一个是数字索引,一个字符索引
}
复制代码
改成同一类型就没有问题了
interface NotOkay {
[x: number]: Animal;
[x: string]: Animal;
}
复制代码
不指定类型的属性,默认就是 string
interface NotOkay {
[x: string]: string;
name: Dog; // Error name 默认就是 string
}
复制代码
interface ClockInterface {
currentTime: Date;
setTime(time:string):void;
}
class Clock implements ClockInterface {
constructor(public currentTime:Date) { }
setTime (item:string) {}
}
复制代码
这么写限制了类 Clock ,必须实现 ClockInterface 中定义的属性 currentTime 以及方法 setTime。
类的类型有两部分组成:静态部分的类型和实例的类型
接口描述的类型是类的实例部分,不会检查类的静态部分类型
interface ClockConstructor {
new (hour: number, minute: number):any;
}
class Clock implements ClockConstructor {
constructor(h: number, m: number) { }
}
复制代码
此时会报错,提醒你 Clock ”提供的内容与签名“new (hour: number, minute: number): any” 不匹配,缘由是 constructor
是类的静态部分类型,不在检查的范围内
要解决这些问题,能够直接操做类的静态部分
interface ClockConstructor {
new (hour: number, minute: number):any;
}
interface ClockInterface {
tick(): void;
}
function createClock(ctor: ClockConstructor, hour: number, minute: number) {
return new ctor(hour, minute); // 主要是理解这里的 new 操做,至关因而把以前的静态部分直接进行实例化了,此时就能校验参数正确与否
}
// 对静态部分进行类型校验
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) {} // 静态部分不作校验,索引此处构造方法不一致也不会报错
tick() {}
}
let digital = createClock(DigitalClock, 12, 17);
/// createClock 第一个参数就进行了检测
复制代码
简化些,可使用类表达式
interface ClockConstructor {
new (hour: number, minute: number):void;
}
interface ClockInterface {
tick():void;
}
const Clock: ClockConstructor = class Clock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("beep beep");
}
};
复制代码
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = <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>{};
复制代码
接口能够同时作为函数和对象使用,并带有额外的属性
/// 下面是接口定义给函数使用
interface Counter {
(start: number): string;
}
let counter:Counter = function(start: number) {return String(start)};
...
/// 下面是定义了给对象使用
interface Counter {
interval: number;
reset(): void;
}
let counter:Counter = {
interval: 23,
reset () {}
}
...
/// 下面是混合,即给对象又给函数使用
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;
}
复制代码
当接口继承一个类类型时,它会继承类的成员。接口一样会继承类的 private 和 protected 成员,这意味着你建立一个接口继承了一个拥有私有或受保护的成员,这个接口类型只能被这个类或其子类所实现
class Control {
private state: any; // state 是私有属性
}
/// SelectableControl 继承了 Control 有私有属性
interface SelectableControl extends Control {
select(): void;
}
/// TextBox 继承至 Control 和接口没什么关系
class TextBox extends Control {
select() { } // 普通类的继承
}
/// Button 继承至 Control 并要实现接口 因其继承 Control,接口能够正常实现
class Button extends Control implements SelectableControl {
select() { } // 普通类继承并实现接口
}
/// Image 并不是继承至 Control,因此没法实现这个有特殊要求的接口
class Image implements SelectableControl {
select() { } // 错误:“Image”类型缺乏“state”属性,须要实现私有属性
}
复制代码
基本概念 | 含义 |
---|---|
类(Class) | 定义了一件事物的抽象特色,包含它的属性和方法 |
对象(Object) | 类的实例,经过 new 生成 |
面向对象(OOP)的三大特性 | 封装、继承、多态 |
封装(Encapsulation) | 将对数据的操做细节隐藏起来,只暴露对外的接口。外界调用端不须要(也不可能)知道细节,就能经过对外提供的接口来访问该对象,同时也保证了外界没法任意更改对象内部的数据 |
继承(Inheritance) | 子类继承父类,子类除了拥有父类的全部特性外,还有一些更具体的特性 |
多态(Polymorphism) | 由继承而产生了相关的不一样的类,对同一个方法能够有不一样的响应。好比 Cat 和 Dog 都继承自 Animal,可是分别实现了本身的 eat 方法。此时针对某一个实例,咱们无需了解它是 Cat 仍是 Dog,就能够直接调用 eat 方法,程序会自动判断出来应该如何执行 eat |
存取器(getter & setter) | 用以改变属性的读取和赋值行为 |
修饰符(Modifiers) | 修饰符是一些关键字,用于限定成员或类型的性质。好比 public 表示公有属性或方法 |
抽象类(Abstract Class) | 抽象类是供其余类继承的基类,抽象类不容许被实例化。抽象类中的抽象方法必须在子类中被实现 |
接口(Interfaces) | 不一样类之间公有的属性或方法,能够抽象成一个接口。接口能够被类实现(implements)。一个类只能继承自另外一个类,可是能够实现多个接口 |
使用 class 定义类,使用 constructor 定义构造函数,经过 new 生成新实例的时候,会自动调用构造函数。
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
复制代码
使用 extends 关键字实现继承,子类中使用 super 关键字来调用父类的构造函数和方法
class Animal {
move(distanceInMeters: number = 0) {
console.log(`Animal moved ${distanceInMeters}m.`);
}
}
class Dog extends Animal {
bark() {
console.log('Woof! Woof!');
}
}
const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();
复制代码
使用 getter 和 setter 能够改变属性的赋值和读取行为
class animal {
constructor (public name:string, public food:string) {}
get eat () {
return this.food
}
set eat (value) {
this.food = value;
}
}
复制代码
ts中使用存取器,有亮点须要注意:
ES7 的提案
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) { }
}
复制代码
origin就是一个静态属性,访问静态属性时,须要加相关类名Grid.origin
,在继承类中也如此使用
class Foo extends Grid {
getOrigin () {
console.log(Grid.origin.x)
}
}
let foo = new Foo(1.2);
foo.getOrigin();
复制代码
与静态属性相似,static
添加到 方面名前,那么这个方法只能经过类调用
class Animal {
static eat() {
console.log("Animal eat food");
}
}
Animal.eat()
let dog = new Animal();
dog.eat(); // error 静态方法不能在实力化对象上调用
复制代码
ES7 提案
class Animal {
food = '米饭' // 在类中直接定义属性
}
let dog = new Animal();
console.log(dog.food);
复制代码
class Greeter {
public greeting: string;
public constructor(message: string) {
this.greeting = message;
}
public greet() {
return "Hello, " + this.greeting;
}
}
复制代码
这里加不加 public 都可
private表示私有,私有的意思就是除了class本身以外,任何人都不能够直接使用
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
class Rhino extends Animal {
constructor() { super("Rhino"); }
}
let animal = new Animal("Goat");
let rhino = new Rhino();
console.log(rhino.name); // 报错,提醒name是私有属性,只能在Animal中使用
复制代码
protected 对于子女、朋友来讲,就是 public 的,能够自由使用,没有任何限制
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}.`; // 这里的this.name就是Person中受保护的成员
}
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 报错,提醒属性“name”受保护,只能在类“Person”及其子类中访问
复制代码
这个受保护还能够用到 constructor 上,造成一种只可被继承后实例化,不能够直接实例化
class Person {
protected name: string;
protected constructor(theName: string) { this.name = theName; }
}
// Employee 可以继承 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"); // 报错,类“Person”的构造函数是受保护的,仅可在类声明中访问
复制代码
使用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"; // 错误! name 是只读的.
复制代码
可使用参数属性,把声明和赋值合并至一处
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {
}
}
复制代码
abstract
既能够用于定义抽象类,也可在内部定义抽象方法,若是定义了抽象方法则要求必须在子类中实现abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log('roaming the earch...');
}
}
class Dog extends Animal { // 抽象类做为其余派生类的基类使用
makeSound () {} // 抽象方法必须在子类中实现,move 方法非必须
}
let dog = new Animal(); // Error 没法建立抽象类的实例
复制代码
class Octopus {
readonly name: string | undefined;
readonly numberOfLegs: number | undefined;
constructor (readonly theName: string) {
console.log(theName);
}
}
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter: Greeter; // 这里限定了 greeter的类型是一个类
greeter = new Octopus("world"); // Erroe 类型不符
复制代码
JavaScript 中,有两种常见的定义函数的方式:函数声明 和 函数表达式
// 函数声明
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; };
...
/// 为了方便查看,能够在接口中定义函数类型
interface myAddInter {
(x: number, y: number):number // 注意此时无须使用 => 符号
}
let myAdd: myAddInter = function(x: number, y: number): number { return x + y; };
复制代码
(x: number, y: number) => number
这个就指定了参数类型以及函数返回类型,若是函数没有明确的返回值,能够指定这个值为void
。函数指定类型和函数中的参数名称没有必要一一对应
留意 ts 中的 => 在函数类型定义中是表示函数的定义,并不是箭头函数
ts 中每一个函数参数都是必须的,因此在函数调用时,多传或者少传参数都是不容许的,若是某些参数是非必须的,能够设置为可选参数
function buildName(firstName: string, lastName?: string) {
if (lastName)
return firstName + " " + lastName;
else
return firstName;
}
复制代码
可选参数必须跟在必须参数后面。 上例 lastName
是可选参数,那么就必须放在 firstName
的后面
function buildName(firstName: string, lastName = "Smith") {
console.log(`${firstName} - ${lastName}`);
}
buildName("Mr.");
buildName("Mr.", "Green");
buildName("Mr.", undefined);
/// 默认值,只有在对应值为 undefined 才会生效
复制代码
默认参数无须放到参数结尾
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
复制代码
...restOfName
就是表示剩余参数数组,在内部能够直接使用这个参数
this
的值在函数被调用时才会去指定,=>
能保存函数建立时的 this
值,而不是调用时的值
是重载容许一个函数接受不一样数量或类型的参数时,做出不一样的处理
/// 好比这里有两个针对不一样传参类型的加法
function add (a:number, b:number) {
return a + b;
}
...
function add (a:string, b:string) {
return Number(a) + Number(b);
}
...
function add (a:number, b:number):number;
function add (a:string, b:string):number;
function add(a: number | string, b: number |string):any {
if (typeof a === 'number' && typeof b === 'number') {
return a + b;
}
if (typeof a === 'string' && typeof b === 'string') {
return Number(a) + Number(b)
}
}
...
/// 下面两个才是函数的重载列表
function add (a:number, b:number):number;
function add (a:string, b:string):number;
...
/// 下面是函数的实现,重载列表 中表述精确的类型,最终函数的实现只是针对全部类型的兜底
function add(a: number | string, b: number |string):any {
复制代码
type Easing = "ease-in" | "ease-out" | "ease-in-out";
type numLiteral = 1 | 2 | 3 | 4;
复制代码
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性,泛型只用于表示类型而不是值
/// 假定有一个函数是根据传入参数建立数组,并返回这个建立的数组,最开始只须要建立string 类型数组
function createArray (length:number, value:string):string[] {
let arr = [];
for (let i = 0; i++; i < length) {
arr[i] = value
}
return arr;
}
...
/// 可是一段时间以后还须要支持 number 数组,由于以前参数类型上的不一样两个函数功能没有差异,这种场景就最适合使用泛型(若是使用 any 就起不到类型检查的功能)
function createArray (length:number, value:number):number[] {
let arr = [];
for (let i = 0; i++; i < length) {
arr[i] = value
}
return arr;
}
...
/// 泛型使用,先在函数名后添加了 <T>,其中 T 用来指代任意输入的类型,而后在函数内就可使用 T 表示相关类型
/// T 此处是 类型变量,这种变量只用于表示类型而不是值
function createArray<T> (length:number, value:T):T[] {
let arr = [];
for (let i = 0; i++; i < length) {
arr[i] = value
}
return arr;
}
复制代码
定义了泛型函数后,能够用两种方法使用
/// 第一种是,传入全部的参数,包含类型参数
createArray<string>(3, 'x'); // 这种调用方式,是明确指定了 T 是 string 类型
/// 第二种利用了 类型推论 编译器会根据传入的参数自动地帮助咱们肯定 T 的类型:
createArray(3, 4);
复制代码
function createArray<T = string> (length:number, value:T):T[] {
let arr = [];
for (let i = 0; i++; i < length) {
arr[i] = value
}
return arr;
}
/// 能够可泛型设定一个默认值
复制代码
泛型是一个不肯定的类型,在函数内部使用泛型变量的时候,因为事先不知道它是哪一种类型,因此不能随意的操做它的属性或方法
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: 传入类型 T 不必定有 length
return arg;
}
...
/// 能够指定入参为数组
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // 正常,数组有length属性
return arg;
}
...
/// 定义数组类型也可使用 Array<> 的形式
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
复制代码
除了指定入参类型,还能够类型使用泛型约束
interface Lengthwise {
length: number
} // 接口对应的是约束条件
/// <T extends Lengthwise> 经过 extends 来实现约束,含义是类型变量 T 继承至 Lengthwise,其中必须包含 length 属性
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // 已经对泛型进行了约束,因此必然有 length
return arg;
}
loggingIdentity(['2'])
复制代码
多个类型参数之间也能够互相约束,下面就约束 U、V 都是 T 的属性,能保证 selElem1和 selElem2 必然存在于 obj 上
function concat<T, U extends keyof T, V extends keyof T> (obj:T, selElem1:U, selElem2:V) {
console.log(`${obj[selElem1]} - ${obj[selElem2]}`)
}
复制代码
/// 泛型函数与普通函数基本没有什么区别,多了一个 类型参数
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <T>(arg: T) => T = identity;
...
/// 还可使用带有调用签名的对象字面量来定义泛型函数
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;
复制代码
还能够把泛型参数看成整个接口的一个参数
/// GenericIdentityFn<number> 比 GenericIdentityFn 更能直观的知道对应的类型参数是什么
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
复制代码
泛型类使用 <>
括起泛型类型,跟在类名后面
class GenericNumber<T> {
zeroValue: T | undefined;
add: ((x: T, y: T) => T) | undefined
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
复制代码
类有两部分:静态部分和实例部分,泛型类指的是实例部分的类型,因此类的静态属性不能使用这个泛型类型。
class GenericNumber<T> {
static standardGreeting:T; // 会提示 静态成员不能引用类类型参数
zeroValue: T | undefined;
add: ((x: T, y: T) => T) | undefined
}
复制代码
枚举(Enum)是一些带名字的常量,用于取值被限定在必定范围内的场景(好比一周七天,接口状态码),ts 支持数字和字符串两种枚举。
/// 数字枚举
/// 枚举成员若是第一个值没有赋值,则被赋予 0
/// 若是第一个值被赋予某项数字常量,则后续成员在上一个枚举成员值加 1
enum Direction {
Up = 1, // 若是不指定从0开始递增
Down, // 递增为 2
Left, // 3
Right // 4
}
...
/// 能够经过枚举属性直接访问枚举成员
Direction.Down // 2
...
/// 字符串枚举
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
复制代码
枚举定义分为自增赋值和手动赋值两种:自增赋值用于数字枚举,数字按步长 1 递增;手动赋值二者都可使用,只是若是对字符串赋值,那就要求对其后的全部值都要有初始化表达式。
/// 下面这段代码,编译器会提醒 Wed 后的值要设置初始化表达式(字符串没有自增的行为)
enum Days {Sun = 1, Mon, Tue, Wed = 'a', Thu, Fri, Sat};
...
/// 若是就是但愿把二者混在一块儿使用(异构枚举,官网列出这不是一种推荐用法),能够先定义手动赋值部分
enum Days {Wed = 'a', Sun = 1, Mon, Tue, Thu, Fri, Sat};
...
/// 初始值能够为小数,后续依然按步长 1 递增
enum Days {Sun = 0.1, Mon, Tue, Wed, Thu, Fri, Sat};
复制代码
枚举成员的值能够是常数(constant member)或计算所得项(computed member)
enum FileAccess {
// 常数
None,
Read = 1 << 1, // 使用了二元运算符
Write = 1 << 2,
ReadWrite = Read | Write, // 引用了以前定义常量以及二元运算符
// 计算所得项
G = "123".length
}
复制代码
枚举成员被看成是常数的条件 * 不具备初始化表达式而且以前的枚举成员是常数(简单讲就是没有赋值,前一个是一个数字常量),此时值为上一个常量值加
1
,若是这个值处于枚举第一位,则其值为0
* 枚举成员使用了常数枚举表达式初始化(常数枚举表达式式是 TypeScript 表达式的子集,它能够在编译阶段求值),知足如下任意条件,就是常数枚举表达: * 数字字面量或字符串字面量 * 引用以前定义的常量枚举成员 * 带括号的常数枚举表达式 *+
,-
,~
一元运算符应用于常数枚举表达式 *+
,-
,*
,/
,%
,<<
,>>
,>>>
,&
,|
,^
二元运算符,常数枚举表达式作为其一个操做对象(若求值后为 NaN 或 Infinity,则会在编译阶段报错)
enum ShapeKind {
Circle,
Square,
}
interface Circle {
kind: ShapeKind.Circle; // 指定接口成员 kind 类型是一个枚举成员
radius: number;
}
let c: Circle = { // 指定 c 的类型是 Circle
kind: ShapeKind.Square, // 不能将类型``ShapeKind.Square``分配给类型``ShapeKind.Circle``
radius: 100,
}
复制代码
枚举做为类型就是每一个枚举成员的联合
enum E {
Foo,
Bar,
}
// x 的类型是枚举对象 E,它的取值只能是其中的成员
function f(x: E) {
// Error! 判断完 x 不为 E.Foo 后,那么 x 就只能为 E.Bar ,因此这个判断的后续没有存在的必须
if (x !== E.Foo || x !== E.Bar) {
// ~~~~~~~~~~~
}
}
复制代码
ts 的代码,能够很直观的分为编译时和运行时两个状态:
enum E {
X, Y, Z
}
function f(obj: { X: number }) {
return obj.X;
}
f(E); // E 符合 类型 {x: number},因此能够正常传值
复制代码
keyof
来获取全部 key 值,应该使用 keyof typeof
来获取enum LogLevel {
ERROR, WARN, INFO, DEBUG
}
...
/// 等同于: type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
type LogLevelStrings = keyof typeof LogLevel;
复制代码
数字枚举成员还具有反向映射,从枚举值到枚举名字,字符串枚举不支持反向映射
enum Enum {
A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
复制代码
普通枚举会把代码逻辑加到转换后的文件中
enum Enum {
A,
B
}
...
/// 转换后的代码
"use strict";
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
Enum[Enum["B"] = 1] = "B";
})(Enum || (Enum = {}));
var info = [Enum.A, Enum.B];
复制代码
const枚举
会避免生成额外的代码,但要求枚举成员只能是常数,不能是计算所得项
const enum Enum {
A = '2',
B = '3'
}
let info = [Enum.A, Enum.B];
...
/// 转换后的代码
"use strict";
var info = ["2" /* A */, "3" /* B */];
复制代码
环境枚举(Ambient Enums)是使用 declare enum
定义的枚举类型,declare
定义的类型只会用于编译时的检查,编译结果中会被删除
enum Enum {
A = 1,
B = '2222'.length
}
console.log(Enum.A);
console.log(Enum.B);
...
/// 转换为
"use strict";
var Enum;
(function (Enum) {
Enum[Enum["A"] = 1] = "A";
Enum[Enum["B"] = '2222'.length] = "B";
})(Enum || (Enum = {}));
console.log(Enum.A);
console.log(Enum.B);
复制代码
declare enum Enum {
A = 1,
B = '2222'.length // 在环境枚举声明中,成员初始化表达式必须是常数表达式
}
console.log(Enum.A);
console.log(Enum.B);
复制代码
declare enum Enum {
A = 1,
B
}
console.log(Enum.A);
console.log(Enum.B);
...
/// 转换后
"use strict";
console.log(Enum.A);
console.log(Enum.B);
复制代码
const
配合使用,直接输出编译结果declare const enum Enum {
A = 1,
B
}
console.log(Enum.A);
console.log(Enum.B);
...
/// 转换为
"use strict";
console.log(1 /* A */);
console.log(2 /* B */);
复制代码
TypeScript使用的是一种结构化的类型检查系统 structural typing,判断两个类型是否兼容,只须要判断他们的“结构”是否一致,也就是说结构属性名和类型是否一致。
子类型(subtyping)是一种类型多态的形式。这种形式下,子类型能够替换另外一种相关的数据类型(超类型 supertype)。若是 S 是 T 的子类型,这种子类型关系一般写做 S <: T
,意思是在任何须要使用 T 类型对象的环境中,均可以安全地使用 S 类型的对象。
通常性对象“鸟”(或超类型)引起了三个派生对象(或子类型)“鸭子”、“杜鹃”和“鸵鸟”。每一个都以本身的方式改变了基本的“鸟”的概念,但仍继承了不少“鸟”的特征。
/// 此处的子类型理解起来有点绕脑,我是参考维基百科举的例子进行理解,把子类型简单理解为在超类基础上进行了扩展
复制代码
子类型与面向对象语言中(类或对象)的继承是两个概念。子类型反映了类型(即面向对象中的接口)之间的关系;而继承反映了一类对象能够从另外一类对象创造出来,是语言特性的实现。所以,子类型也称接口继承;继承称做实现继承。
子类就是实现继承,子类型就是接口继承
(以上部份内容来源于维基百科定义)
子类型在编程语言实现方面,分为两种:
/// ts 里的类型兼容性是基于结构子类型的
interface Named {
name: string | undefined;
}
class Person {
name: string | undefined;
}
let p: Named; // 定义 p 的类型是 Named
p = new Person(); // 实现时,使用Person实现,二者有相同的结构,因此类型之间兼容
复制代码
若是 x 要兼容 y(y 能够赋值给 x),那么 y类型
要是 x类型
的 子类型
interface Named {
name: string;
}
let x: Named;
let y = { name: 'Alice', location: 'Seattle' };
x = y; // y 对应的类型是 x 对应类型的子类型
...
function greet(n: Named) {
console.log('Hello, ' + n.name);
}
greet(y); // y 对应的类型是 n 对应类型的子类型
复制代码
函数间的兼容规则,要看参数、返回值之间关系再作判断
let x = (a: number) => 2;
let y = (b: number, s: string) => 0;
y = x; // OK
x = y; // Error 不能将类型“(b: number, s: string) => number”分配给类型“(a: number) => number”
复制代码
此时,主要看下参数列表, x 的每一个参数的类型在 y 里找到对应类型的参数,因此能够赋值成功(名字无所谓,类型能对上便可),可是反过来不行
let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});
y = x; // Error, 不能将类型“() => { name: string; }”分配给类型“() => { name: string; location: string; }”。
x = y; // OK
复制代码
此时,强制源函数的返回值类型
必须是目标函数返回值类型
的子类型
x 返回值的类型是 { name: string; }
,y 返回值的类型是 { name: string; location: string; }
,y 返回值类型 是 x返回值类型 的子类型,因此 y 能赋值给 x,反过来不行
此处参考 深刻类型系统_typescript笔记8 聊聊TypeScript类型兼容,协变、逆变、双向协变以及不变性
双变是指同时知足协变和逆变:
interface Animal {
base: string;
}
interface Dog extends Animal {
addition: string;
};
// 子类型
let Animal: Animal = { base: 'base' };
let Dog: Dog = { base: 'myBase', addition: 'myAddition' };
Animal = Dog; // Dog 是 Animal 的子类型
// 协变
type Covariant<T> = T[];
let coAnimal: Covariant<Animal> = [];
let coDog: Covariant<Dog> = [];
coAnimal = coDog; // 子类能够赋值给父类
// 逆变 --strictFunctionTypes true
type Contravariant<T> = (p: T) => void;
let contraAnimal: Contravariant<Animal> = function(p) {}
let contraDog: Contravariant<Dog> = function(p) {}
contraDog = contraAnimal; // 父类能够赋值给子类
// 双变
type Bivariant<T> = {
compare(p: T): void
};
declare let biAnimal: Bivariant<Animal>;
declare let biDog: Bivariant<Dog>;
// both are ok
biDog = biAnimal;
biAnimal = biDog; // 父类子类相互赋值
复制代码
里氏替换原则(Liskov Substitution principle)是对子类型的特别定义,派生类(子类)对象能够在程序中代替其基类(超类)对象
ts 的函数相关规则在设定时,考虑到 js 规则复杂,就把参数类型设置为双向协变。在比较两个函数类型时,只要一方参数兼容另外一方的参数便可,就可以相互赋值(也就是平时咱们在使用 js 时,历来不去考虑函数的形参与实参是否匹配的状况,函数参数时原始数据时,只考虑个数对不对,参数是对象时,属性值是多了仍是少了都不要紧,必要值不存在时,运行时再报错)
比较参数兼容性时,不要求匹配可选参数,对于剩余参数,就当成是无限多个可选参数,也不要求严格匹配。
enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };
let s = Status.Ready;
s = Color.Green; // Error 不能将类型“Color.Green”分配给类型“Status”,两个不一样的枚举类型
复制代码
enum Status { Ready, Waiting };
// 数值兼容枚举值
let ready: number = Status.Ready;
// 枚举值兼容数值
let waiting: Status = 1;
复制代码
enum Status { Ready = '1', Waiting = '0' };
let ready: string = Status.Ready;
let waiting: Status = '0'; // 报错 不能将类型“"0"”分配给类型“Status”
复制代码
比较两个类类型的对象时,只有实例的成员会被比较,静态成员和构造函数不在比较范围内
class Animal {
static id: string = 'Kitty';
feet: number | undefined;
constructor(name: string, numFeet: number) { }
}
class Size {
feet: number | undefined;
constructor(numFeet: number) { }
}
let a: Animal = new Animal('dog', 2);
let s: Size = new Size(1);
a = s; // OK
s = a; // OK
复制代码
对于泛型的比较,要看有没有使用泛型参数,对于没指定泛型类型的泛型参数时,会把全部泛型参数当成any比较
interface Empty<T> {
}
declare let x: Empty<number>;
declare let y: Empty<string>;
x = y;
...
interface NotEmpty<T> {
data: T;
}
declare let x: NotEmpty<number>;
declare let y: NotEmpty<string>;
x = y; // Error, 不能将类型“Empty<string>”分配给类型“Empty<number>”。
复制代码
交叉类型是将多个类型合并为一个类型, 这让咱们能够把现有的多种类型叠加到一块儿成为一种类型,它包含了所需的全部类型的特性。
interface foo {
foo: string
}
interface bar {
bar: number
}
/// foo & bar 是一个交叉类型,包含 两个类型全部特性
let baz: foo & bar = {
foo: '',
bar: 0
}
复制代码
联合类型表示一个值能够是几种类型之一,用 |
进行链接,联合类型 A | B
,表示类型要么是 A 要么是 B;
let info: string | number;
info = '';
info = 23;
info = true; // 不能将类型“true”分配给类型“string | number”。
...
interface foo {
name: string,
age: number
}
interface bar {
name: string,
flag: boolean
}
let baz: foo | bar = {
name: '',
age: 23
}
/// baz 的类型是 foo | bar 的联合类型,因此 baz 的取值,要么是 foo 类型,要么是 bar 类型
复制代码
若是一个值是联合类型,咱们只能访问此联合类型的全部类型中共有成员
interface Bird {
fly():void;
layEggs():void;
}
interface Fish {
swim():void;
layEggs():void;
}
function getSmallPet(): (Fish | Bird) & void {
// ...
}
let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim(); // errors, 此时 ts 不能明确的确认swim方法必然存在,因此会报错
复制代码
联合类型至关于由类型构成的枚举类型,于是没法肯定其具体类型,因此 pet.swim()
的访问会报错,若是想要确切的访问某个类的方法,可使用类型断言
if ((<Fish>pet).swim) {
(<Fish>pet).swim();
}
else {
(<Bird>pet).fly();
}
复制代码
除了使用断言,还可使用类型保护
来实现上述代码正常工做。类型保护本质是把联合类型从一个宽的类型,具体到一个窄的类型。
typeof variable === 'type'
能肯定基本类型,若是用在联合类型中,能自动缩窄对应分支下的联合类型
/// 函数参数对应的类型是 number | string
let add = function (ad:number | string) {
/// 这里经过 typeof 进行了类型收窄,这里判断是联合类型中的 number
if (typeof ad === 'number') {
} else {
/// 这里则必然是 string
console.log(ad.length)
}
}
复制代码
typeof
的类型保护只能用于 number
、string
、boolean
、symbol
,针对其余类型是不安全的
相似于 typeof
只不过 instanceof
使用范围更广
declare let x: Date | RegExp;
if (x instanceof RegExp) {
// 正确 instanceof类型保护,自动缩窄到RegExp实例类型
x.test('');
}
else {
// 正确 自动缩窄到Date实例类型
x.getTime();
}
复制代码
instanceof 右侧是个构造函数,此时左侧类型会被缩窄到:
// Case1 该类实例的类型
declare let x:any;
if (x instanceof Date) {
// x 从 any 缩窄到 Date
x.getTime(); // x 类型为 Date
}
// Case2 由构造函数返回类型构成的联合类型
interface DateOrRegExp {
new(): Date;
new(value?: string): RegExp;
}
declare let A: DateOrRegExp;
let y:any;
if (y instanceof A) {
y; // y 从 any 缩窄到 RegExp | Date
}
复制代码
typeof
与 instanceof
类型保护可以知足通常场景,对于一些更加特殊的,能够经过自定义类型保护
来缩窄类型。 自定类型保护
与普通函数声明相似,只是返回类型部分是个类型谓词(type predicate)
interface RequestOptions {
url: string;
onSuccess?: () => void;
onFailure?: () => void;
}
// 自定义类型保护,将参数类型 any 缩窄到 RequestOptions
function isValidRequestOptions(opts: any): opts is RequestOptions {
return opts && opts.url;
}
let opts:any;
if (isValidRequestOptions(opts)) {
// opts从any缩窄到RequestOptions
opts.url;
}
复制代码
opts is RequestOptions
是类型谓词
/// ! 后缀 能够去掉| undefined | null
let x: string | undefined | null;
x!.toUpperCase();
复制代码
strictNullChecks
,当你声明一个变量时不会自动包含 null 或 undefinedlet s = "foo";
s = null; // 错误, 不能将类型“null”分配给类型“string”
let sn: string | null = "bar";
sn = null; // 能够
sn = undefined; // 错误, 不能将类型“undefined”分配给类型“string | null”
/// tsconfig.json 中设置 "strictNullChecks": false 就能够关闭这个检查
复制代码
ts 会把 null和 undefined区别对待。 string | null
, string | undefined
和 string | undefined | null
是不一样的类型。
默认状况下可选参数会被自动地加上 | undefined
/// 类型推导为 f(x: number, y?: number | undefined): number
function f(x: number, y?: number) {
return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
...
/// tsconfig.json 中设置 "strictNullChecks": false 时
/// 类型推导为 f(x: number, y?: number): number,可选参数的 undefined 就不会被加上
...
/// 是否设置 strictNullChecks 对 undefined 的使用没有影响,影响的是 null
f(1, undefined); // 都可编译通知
f(1, null); // 设为 false 时编译经过,默认状况下提示 类型“null”的参数不能赋给类型“number | undefined”的参数
复制代码
| undefined
class C {
a: number; // 默认状况,此处会报错,属性“a”没有初始化表达式,且未在构造函数中明确赋值。类型推导为 C.a: number
b?: number; // 可选属性的类型推导为 C.b?: number | undefined
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 不能将类型 “undefined” 分配给类型 “number”
c.b = 13;
c.b = undefined; // ok
...
/// tsconfig.json 中设置 "strictNullChecks": false 时,上面报错的就不会再报错
复制代码
类型别名能为现有类型建立一个别名,从而加强其可读性
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === 'string') {
return n;
}
else {
return n();
}
}
复制代码
类型别名在定义和使用上和接口很接近
interface Animal {
name: string
}
interface Dog extends Animal {
age: number
}
...
type Animal = {
name: string
}
type Dog = Animal & {
age: number
}
复制代码
与接口的区别在于:
type Alias = { num: number }
interface Interface {
num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;
复制代码
鼠标放到Alias上看到的信息是
/// Alias 只是一个集合,并非新的类型
type Alias = {
num: number;
}
复制代码
也就是其定义的内容,若是放到 Interface
/// Interface 是一个新的类型
interface Interface
复制代码
interface Animal {
name: string
}
interface Dog extends Animal {
age: number
}
class erha implements Dog {
constructor (public name:string, public age:number) {}
}
...
/// 类型别名能够被继承并生成新的交叉类型
type Animal = {
name: string
}
type Dog = Animal & {
age: number
}
class erha implements Dog {
constructor (public name:string, public age:number) {}
}
复制代码
应用场景上,两者区别以下:
字符串字面量类型容许你指定字符串必须的固定值
type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
animate(dx: number, dy: number, easing: Easing) {
if (easing === "ease-in") {
// ...
}
else if (easing === "ease-out") {
}
else if (easing === "ease-in-out") {
}
}
}
let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error 类型“"uneasy"”的参数不能赋给类型“Easing”的参数。
复制代码
字符串字面量类型还能够用于区分函数重载
function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
// ... code goes here ...
}
复制代码
type num = 1 | 2 | 3 | 4;
let nm1: num = 3;
复制代码
/// 联合枚举
enum E {
Foo,
Bar
}
/// 枚举类型
function f(x: E) {
if (x !== E.Foo) {
}
}
...
/// 字面量
function f(x: 'Foo' | 'Bar') {
if (x !== E.Foo) {
}
}
复制代码
这里用字符串字面量联合类型 'Foo' | 'Bar'
模拟枚举 E
,从类型角度来看,联合枚举就是由数值/字符串字面量
构成的枚举,联合枚举,即数值/字符串联合
枚举成员类型与数值/字符串字面量类型也叫单例类型
一个单例类型下只有一个值,例如字符串字面量类型'Foo'只能取值字符串'Foo'
结合单例类型,联合类型,类型保护和类型别名可建立一个叫作 可区分联合 高级模式,它也称作 标签联合
或 代数数据类型
,其可运算、可进行逻辑推理的类型,一个可区分联合具备如下三部分组成:
/// 一些具备公共单例属性(kind)的类型,``kind`` 属性称作可辨识的特征或标签,其它的属性则特定于各个接口,目前各个接口之间没有联系
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
...
/// 定义联合类型,并起个别名
type Shape = Square | Rectangle | Circle;
复制代码
使用可区分联合,经过区分公共单例属性的可缩窄父类型,达到类型保护的能力:
function area(s: Shape) {
switch (s.kind) {
/// 自动缩窄到 "square"
case "square": return s.size * s.size;
/// 自动缩窄到 "rectangle"
case "rectangle": return s.height * s.width;
/// 自动缩窄到 "circle"
case "circle": return Math.PI * s.radius ** 2;
}
}
复制代码
与 instanceof类型保护 的区别:
/// 修改类型别名,添加 Triangle
type Shape = Square | Rectangle | Circle | Triangle;
function area(s: Shape) {
switch (s.kind) {
/// 自动缩窄到 "square"
case "square": return s.size * s.size;
/// 自动缩窄到 "rectangle"
case "rectangle": return s.height * s.width;
/// 自动缩窄到 "circle"
case "circle": return Math.PI * s.radius ** 2;
/// 此处遗漏了 对 Triangle 的类型处理,
}
}
复制代码
当没有涵盖全部可辨识的变化,可使用两种方式进行完整性检查
--strictNullChecks
而且指定一个返回值类型,函数调用默认会返回 undefined ,若是指定了返回类型,没有处理的分支,就会返回 undefined ,这样编译阶段就能保证完整性"strictNullChecks": true
function area(s: Shape): number { // error: 函数缺乏结束返回语句,返回类型不包括 "undefined"
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
}
}
复制代码
function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
default: return assertNever(s); // error 类型“Triangle”的参数不能赋给类型“never”的参数。
}
}
复制代码
这里, assertNever 检查 s 是否为 never 类型—即为除去全部可能状况后剩下的类型。
/// JavaScript
class A {
foo() { return this }
}
class B extends A {
bar() { return this }
}
new B().foo().bar();
/// 在 JavaScript 运行时经过 this 造成链式调用
...
/// A类实例类型 类型推导 (method) A.foo(): A,foo 方法对应的 this 为 A
new A().foo();
/// B类实例类型 类型推导 (method) A.foo(): B,foo 方法对应的 this 为 B
new B().foo();
/// this 的类型并非固定的,取决于其调用上下文
复制代码
此时若是在 ts 中针对 foo 或 bar 添加类型的话,大概只能是 B & A
这种,这并非很合理的类型,针对这种场景,ts 引入了 this类型
,this 表示所属类或接口的子类型 ,这被称为 有界多态性(F-bounded polymorphism),它能很容易的表现连贯接口间的继承。
class BasicCalculator {
public constructor(protected value: number = 0) { }
public currentValue(): number {
return this.value;
}
/// 类型推导为 (method) BasicCalculator.add(operand: number): this
public add(operand: number): this {
this.value += operand;
return this;
}
/// 类型推导为 (method) BasicCalculator.multiply(operand: number): this
public multiply(operand: number) {
this.value *= operand;
return this;
}
// ... other operations go here ...
}
复制代码
好比, 在上面计算器的例子里,在每一个操做以后都返回 this 类型(this 类型可以自动对应到所属类实例类型)
这种JavaScript运行时特性,在TypeScript静态类型系统中一样支持
/// 因为调用时返回了this 类型,此时可使用链式调用
let v = new BasicCalculator(2)
.multiply(5)
.add(1)
.currentValue();
复制代码
新的类能够直接使用以前的方法,不须要作任何的改变
class ScientificCalculator extends BasicCalculator {
public constructor(value = 0) {
super(value);
}
public sin() {
this.value = Math.sin(this.value);
return this;
}
// ... other operations go here ...
}
let v = new ScientificCalculator(2)
.multiply(5)
.sin()
.add(1)
.currentValue();
复制代码
TypeScript中的this类型分为2类:
class this type
:类/接口(的成员方法)中的 this 类型function this type
:普通函数中的 this 类型/// 把this显式地做为函数的(第一个)参数,从而限定其类型,像普通参数同样进行类型检查
declare class C { m(this: this):void; }
let c = new C();
// f 类型为 (this:C) => any
let f = c.m;
// 错误 类型为“void”的 "this" 上下文不能分配给类型为“C”的方法的 "this"
f();
...
/// 去掉显式声明的 this类型
declare class C { m(); }
...
/// 正确
f();
复制代码
箭头函数(lambda)的 this 没法手动限定其类型
索引类型让静态检查可以覆盖到类型不肯定(没法穷举)的”动态“场景
/// pluck 函数能从 o 中摘出来 names 指定的那部分属性
function pluck(o, names) {
return names.map(n => o[n]);
}
复制代码
此处编译时,会报错。须要添加约束,这里须要两个约束条件:
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
return names.map(n => o[n]);
}
interface Person {
name: string;
age: number;
}
let person: Person = {
name: 'Jarid',
age: 35
};
let strings: string[] = pluck(person, ['name']); // ok, string[]
复制代码
keyof T 取类型 T 上的全部 public 属性名构成联合类型
经过 索引类型查询 和 索引访问操做符使用,编译器会检查 name 是否真的是 Person 的一个属性
keyof Person
是彻底能够与 'name' | 'age'
互相替换的。 不一样的是若是你添加了其它的属性到 Person,例如 address: string
,那么 keyof Person
会自动变为 'name' | 'age' | 'address'
keyof 在没法得知(或没法穷举)属性名的场景颇有意义
T[K]
索引访问操做符,直接反应相关类型,这个要确保 K extends keyof T
被正常调用,那么索引访问操做符就能够正常使用,好比 person['name']
就表示 string 类型
keyof 与 T[K] 一样适用于字符串索引签名(index signature)
/// 若是是一个带有字符串索引签名的类型,那么keyof T 会是string | number
interface NetCache {
[propName: string]: object;
}
/// keyType: string | number 如按定义,此处类型应为 string,只不过由于在JavaScript里的数值索引会被转换成字符串索引,好比 arr[0] === arr['0'],因此对应的类型能够是字符串也能够是数值
let keyType: keyof NetCache;
...
/// 若是一个类型带有数字索引签名,那么keyof T为number
interface NetCache {
[propName: number]: object;
}
// keyType: number
let keyType: keyof NetCache;
复制代码
这是一种从旧类型建立新类型的方式,在映射类型里,新类型以相同的形式去转换旧类型里的每一个属性。
interface Person {
name: string;
age: number
}
type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;
...
/// 类型别名 Readonly Partial 存在于 lib.es5.d.ts
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
type Partial<T> = {
[P in keyof T]?: T[P];
}
复制代码
这样就由一个旧类型建立了一个新的类型,上面例子中针对源类型信息都保留了,仅存在修饰符上的差别,这类转换被称为同态转换
type Keys = 'option1' | 'option2';
/// type Flags = { option1: boolean; option2: boolean; }
type Flags = { [K in Keys]: boolean };
复制代码
[K in Keys]
形式上与索引签名相似,只是融合了 for...in
语法,具备三个部分:
Flags
丢弃了源属性值类型,属于非同态(non-homomorphic)转换,非同态类型本质上会建立新的属性,它们不会从它处拷贝属性修饰符映射类型描述的是类型而非成员, 若想添加成员,则可使用交叉类型
type Flags = { [K in Keys]: boolean; } & { newMember: boolean };
复制代码
条件类型用来表达非均匀类型映射(non-uniform type mapping),可以根据类型兼容关系(即条件)从两个类型中选出一个
T extends U ? X : Y
复制代码
/// 当即解析
declare function f<T extends boolean>(x: T): T extends true ? string : number;
// Type is 'string | number let x = f(Math.random() < 0.5) ... /// 推迟解析 interface Foo { propA: boolean; propB: boolean; } declare function f<T>(x: T): T extends Foo ? string : number; function foo<U>(x: U) { // a 的类型为 U extends Foo ? string : number,当有另外一段代码调用foo,它会用其它类型替换U,TypeScript将从新计算有条件类型,决定它是否能够选择一个分支 let a = f(x); let b: string | number = a; } ... // 嵌套的条件类型相似于模式匹配 type TypeName<T> = T extends string ? "string" : T extends number ? "number" : T extends boolean ? "boolean" : T extends undefined ? "undefined" : T extends Function ? "function" : "object"; 复制代码
可分配条件类型(distributive conditional type)中被检查的类型是个裸类型参数(naked type parameter)。其特殊之处在于知足分配律
(A | B | C) extends U ? X : Y
等价于
(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
复制代码
type Diff<T, U> = T extends U ? never : T;
/// T 类型等价于联合类型 "b" | "d"
/// 等价于
/// 'a' extends 'a' | 'c' | 'f' ? never : 'a'
/// 'b' extends 'a' | 'c' | 'f' ? never : 'b'
/// 'c' extends 'a' | 'c' | 'f' ? never : 'c'
/// 'd' extends 'a' | 'c' | 'f' ? never : 'd'
type T = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;
复制代码
Exclude<T, U>
-- 从T中剔除能够赋值给U的类型。Extract<T, U>
-- 提取T中能够赋值给U的类型。NonNullable<T>
-- 从T中剔除null和undefined。ReturnType<T>
-- 获取函数返回值类型。InstanceType<T>
-- 获取构造函数类型的实例类型。若是一个文件中含有合法的 import 或 export 语句,就会被当作模块(拥有模块做用域),不然就将在运行在全局做用域下。
任何声明(好比变量,函数,类,类型别名或接口)都可以经过添加 export 关键字来导出
/// index.ts
export interface Animal {
name: string;
age: number;
}
...
/// main.ts
import { Animal } from './index'
export const dog:Animal = {
name: '',
age: 0
}
export class cat implements Animal {
constructor (public name:string, public age:number) {}
}
复制代码
import/require
会引入目标模块源码,并从中提取类型信息
import { name } from './file';
复制代码
转编码时要指定module
tsc --module commonjs
...
/// 或者在 tsconfig.json 文件中设置 module
"target": "ES5" /* 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"module": "commonjs" /* 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
复制代码
commonjs
NodeJS模块定义amd
AMD (Require.js )system
SystemJSumd
UMDes6
ES Modulees2015
等价于es6esnext
还没有收入ES规范的前沿模块定义,如import(), import.meta
等none
禁用全部模块定义,如import, export等(用到的话会报错)上面模块系统中,除了 es module 和 commonjs 其余已经不怎么使用
// NodeJS模块(CommonJS)
let x = {a: 1};
exports.x = x;
module.exports = x;
复制代码
针对 CommonJS 的 exports 语法,ts 提供了 export =
,对应的导入 import module = require("module")
/// index.ts
let x = { a: 1 };
export = x;
...
/// main.ts
import index = require("./index");
复制代码
要想描述非 TypeScript 编写的类库的类型,咱们须要声明类库所暴露出的 API,在声明文件(d.ts)里定义的,进行补充类型声明
当使用第三方库时,咱们须要引用它的声明文件,才能得到对应的代码补全、接口提示等功能
语法 | 含义 |
---|---|
declare var |
声明全局变量 |
declare function |
声明全局方法 |
declare class |
声明全局类 |
declare enum |
声明全局枚举类型 |
declare namespace |
声明(含有子属性的)全局对象 |
interface 和 type |
声明全局类型 |
export |
导出变量 |
export namespace |
导出(含有子属性的)对象 |
export default |
ES6 默认导出 |
export = commonjs |
导出模块 |
export as namespace |
UMD 库声明全局变量 |
declare global |
扩展全局变量 |
declare module |
扩展模块 |
/// <reference /> |
三斜线指令 |
当 ts 文件引入 第三方库时,第三方库可能会暴露出一些全局变量,好比 $、jQuery
,或者是为这些变量设定类型,能够经过declare var
进行声明。
declare var
并无真的定义一个变量,只是定义了全局变量 jQuery 的类型,仅仅会用于编译时的检查,在编译结果中会被删除
declare const jQuery: (selector: string) => any;
jQuery('#foo');
...
/// 编译为
"use strict";
jQuery("#foo");
复制代码
把声明语句放到一个单独的文件就是声明文件,声明文件必需以 .d.ts
为后缀
/// 单独文件 jQuery.d.ts
declare const jQuery: (selector: string) => any;
...
/// index.ts
jQuery("#foo");
复制代码
ts
会解析项目中全部的 *.ts
文件,当咱们将 jQuery.d.ts
放到项目中时,其余全部 *.ts
文件就均可以得到 jQuery 的类型定义了
若没法解析,能够检查下 tsconfig.json 中的 files、include 和 exclude 配置,确保其包含了 jQuery.d.ts 文件
针对 jQuery 是没有必要这样单独定义的,能够直接使用第三方声明文件 @types/jquery
库的使用场景主要有如下几种:
全局变量
:经过 <script>
标签引入第三方库,注入全局变量npm 包
:经过 import foo from 'foo'
导入,符合 ES6 模块规范UMD 库
:既能够经过 <script>
标签引入,又能够经过 import 导入直接扩展全局变量
:经过 <script>
标签引入后,改变一个全局变量的结构在 npm 包或 UMD 库中扩展全局变量
:引用 npm
包或 UMD
库后,改变一个全局变量的结构模块插件
:经过 <script>
或 import
导入后,改变另外一个模块的结构全局变量的声明文件主要有如下几种语法:
declare var/let/const
声明全局变量declare function
声明全局方法declare class
声明全局类declare enum
声明全局枚举类型declare namespace
声明(含有子属性的)全局对象interface 和 type
声明全局类型定义一个全局变量的类型,全局变量都是禁止修改的常量,因此大部分状况都应该使用 const 而不是 var 或 let
declare const jQuery: (selector: string) => any;
复制代码
声明语句中只能定义类型,切勿在声明语句中定义具体的实现
/// Error 不容许在环境上下文中使用初始化表达式
declare const jQuery = function(selector) {
return document.querySelector(selector);
};
复制代码
定义全局函数的类型
declare function jQuery(selector: string): any;
jQuery("#foo");
复制代码
在函数类型的声明语句中,函数重载也是支持的
declare function jQuery(selector: string): any;
declare function jQuery(domReadyCallback: () => any): any;
jQuery('#foo');
jQuery(function() {
alert('Dom Ready!');
});
复制代码
当全局变量是一个类的时候,使用 declare class
定义类型。declare class
语句也只能用来定义类型,不能用来定义具体的实现,
declare class Animal {
name: string;
constructor(name: string);
sayHi(): string;
}
let cat = new Animal('Tom');
cat.sayHi()
复制代码
使用 declare enum
定义的枚举类型也称做外部枚举 (Ambient Enums)
declare enum Directions {
Up,
Down,
Left,
Right
}
let directions = [
Directions.Up,
Directions.Down,
Directions.Left,
Directions.Right
];
...
/// 编译为
"use strict";
var directions = [
Directions.Up,
Directions.Down,
Directions.Left,
Directions.Right
];
/// 其中 Directions 是由第三方库定义好的全局变量
复制代码
namespace
是 ts 早期时为了解决模块化而创造的关键字,中文称为命名空间,因为 ES module 的普遍使用,ts 的命名空间已经没有使用的意义。
declare namespace
用来表示全局变量是一个对象,包含不少子属性,适用于在全局变量还有子属性的状况
declare function jQuery(selector: string): any;
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
}
jQuery.ajax('/api/get_something');
复制代码
若是对象拥有深层的层级,则须要用嵌套的 namespace 来声明深层的属性的类型
declare namespace jQuery {
namespace fn {
function extend(object: any): void;
}
}
jQuery.fn.extend({
check: function() {}
});
复制代码
能够直接使用 interface 或 type 来声明一个全局的接口或类型
/// jQuery.d.ts
interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
}
declare namespace jQuery {
function ajax(url: string, settings?: AjaxSettings): void;
}
...
/// index.ts
let settings: AjaxSettings = {
method: 'POST',
data: {
name: 'foo'
}
};
jQuery.ajax('/api/post_something', settings);
复制代码
在 jQuery.d.ts
中,AjaxSettings 暴露在命名中间以外,做为全局类型做用于整个项目中,咱们应该尽量的减小全局变量或全局类型的数量,最好将他们放到 namespace 下
declare namespace jQuery {
interface AjaxSettings {
method?: 'GET' | 'POST'
data?: any;
}
function ajax(url: string, settings?: AjaxSettings): void;
}
...
/// 使用时加上 jQuery 前缀,jQuery.AjaxSettings
let settings: jQuery.AjaxSettings = {
复制代码
多个声明语句会合并起来
declare function jQuery(selector: string): any;
declare namespace jQuery {
function ajax(url: string, settings?: any): void;
namespace fn {
function extend(object: any): void;
}
}
复制代码
若是咱们使用是一个 npm 包,能够先看看对应的包有没有声明文件(能够在 npm官网搜索 @types),好比@types/node
、@types/react
、@types/jest
。
没有的话,能够自行编写声明文件,因为是经过 import 语句导入的模块,因此声明文件存放的位置也有所约束,有两种方式:
node_modules/@types/xx/index.d.ts
文件,存放 xx 模块的声明文件。types
目录,专门用来管理本身写的声明文件,将 xx
的声明文件放到 types/xx/index.d.ts
中。这种方式须要配置下 tsconfig.json
中的 paths
和 baseUrl
字段。/// 目录结构
/path/to/project
├── src
| └── index.ts
├── types
| └── xx
| └── index.d.ts
└── tsconfig.json
...
/// tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"baseUrl": "./",
"paths": {
"*": ["types/*"]
}
}
}
复制代码
这样配置后,若是导入 xx 就会先到 types 下寻找有没有声明文件
/// types/foo/index.d.ts
export const name: string;
export function getName(): string;
...
/// index.d.ts
import { name, getName } from 'foo';
/// (alias) const name: string
/// import name
console.log(name); // 这样声明文件能够正常使用
getName();
复制代码
export
导出变量export
的语法与普通的 ts 中的语法相似,区别在于声明文件中禁止定义具体的实现/// types/foo/index.d.ts
/// 不容许在环境上下文中使用初始化表达式
export const name: string = '';
const age: number;
复制代码
/// types/foo/index.d.ts
export const name: string;
declare const age: number;
export { age };
...
/// index.d.ts
/// (alias) const name: string
/// (alias) const age: number
import { name, age } from 'foo';
复制代码
export namespace
用来导出一个拥有子属性的对象/// types/foo/index.d.ts
export namespace foo {
const name: string;
namespace bar {
function baz(): string;
}
}
...
/// index.d.ts
import { foo } from 'foo';
console.log(foo.name);
foo.bar.baz();
复制代码
export default
能够导出一个默认值// types/foo/index.d.ts
export default function foo(): string;
...
// src/index.ts
import foo from 'foo';
foo();
复制代码
只有 function、class 和 interface 能够直接默认导出,其余的变量须要先定义出来,再默认导出
export =
commonjs 导出模块,不建议使用在早期 js 版本,还没有引入模块概念时,会有一种经过匿名函数向现有对象添加内容或建立对象的功能,达成相似命名空间的能力(这种方式能够确保建立的变量不会泄漏至全局变量上)
var something;
(function(something) {
something.foo = 123;
})(something || (something = {}));
console.log(something); // { foo: 123 }
(function(something) {
something.bar = 456;
})(something || (something = {}));
console.log(something); // { foo: 123, bar: 456 }
复制代码
后来随着es module
的兴起,这种写法已经没人用了,不过 ts 中还保留着,能够做为一种组织代码的手段(按官网的说法以前是叫内部模块,但由于怕和模块系统搞混,因此改叫命名空间)
namespace something {
export let foo:number = 123;
export let bar:number = 456;
}
console.log(something); // { foo: 123, bar: 456 }
复制代码
是一种简化命名空间操做的方法,使用import q = x.y.z
给经常使用对象起一个短的名字
namespace something {
export let foo = 123;
export let bar = 456;
}
import foo = something.foo;
console.log(foo);
复制代码
命名空间会存在难以识别组件之间的依赖关系的致命问题,因此除非是移植旧的 js 代码,通常状况下不建议使用,对于新项目来讲推荐使用模块作为组织代码的方式。
ts 兼容 ES Module 规范,简单来说,若是一个文件中含有合法的import或export语句,就会被当作模块(拥有模块做用域,在这个文件中建立一个本地的做用域)。
文件模块在ts中也被称为外部模块,彻底兼容 ES Module 语法
/// index.ts
export const foo = "foo";
...
/// main.ts
import { foo } from "./index";
const bar = foo;
复制代码
通常状况下,import/require
会引入目标模块源码,并从中提取类型信息,因为导入路径不一样,因此存在两种大相径庭的模块(模块的解析主要是对路径的解析):
.
开头,例如:./someFile
或者 ../../someFolder/someFile
等);core-js
,typestyle
,react
或者甚至是 react/core
等)模块解析策略:Node和Classis
可使用 --moduleResolution
标记来指定使用哪一种模块解析策略。若未指定,那么在使用了 --module AMD | System | ES2015
时的默认值为Classic,其它状况时则为Node
。
相对导入模块是相对于导入它的文件进行解析,
/// root/src/folder/A.ts
import { b } from "./moduleB 复制代码
这里的查找 moduleB 的流程为
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
动态查找模块进行解析时,编译器会从包含导入文件的目录开始依次向上级目录遍历,尝试定位匹配的声明文件。
/// root/src/folder/A.ts
import { b } from "moduleB"
复制代码
会按以下的顺序来查找"moduleB"
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
/root/src/moduleB.ts
/root/src/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts
/// root/src/moduleA.js
var x = require("./moduleB");
复制代码
如下面的顺序解析这个导入:
/root/src/moduleB.js
文件是否存在。/root/src/moduleB
目录是否包含一个package.json
文件,且package.json
文件指定了一个"main"
模块。若是发现文件 /root/src/moduleB/package.json
包含了 { "main": "lib/mainModule.js" }
,那么Node.js会引用/root/src/moduleB/lib/mainModule.js
。/root/src/moduleB
目录是否包含一个index.js
文件。 这个文件会被隐式地看成那个文件夹下的"main"模块。解析思路和classic实际上是同样的,只不过会多一些对 package 以及目录的处理
Node 会在一个特殊的文件夹 node_modules
里查找你的模块。 node_modules
可能与当前文件在同一级目录下,或者在上层目录里。 Node会向上级目录遍历,查找每一个 node_modules
直到它找到要加载的模块。
/// root/src/moduleA.js
var x = require("moduleB");
复制代码
会如下面的顺序去解析 moduleB,直到有一个匹配上:
/root/src/node_modules/moduleB.js
/root/src/node_modules/moduleB/package.json (若是指定了"main"属性)
/root/src/node_modules/moduleB/index.js
/root/node_modules/moduleB.js
/root/node_modules/moduleB/package.json (若是指定了"main"属性)
/root/node_modules/moduleB/index.js
/node_modules/moduleB.js
/node_modules/moduleB/package.json (若是指定了"main"属性)
/node_modules/moduleB/index.js
解析思路一致,只是全部的解析是围绕着 node_modules 进行
ts 是模仿 Node 运行时的解析策略来在编译阶段定位模块定义文件。
ts 在 Node 解析逻辑基础上增长了 ts 源文件的扩展名( .ts
,.tsx
和.d.ts
)。 同时,ts 在 package.json
里使用字段"types"
来表示相似"main"
的意义 - 编译器会使用它来找到要使用的"main"
定义文件。(至关于模拟 NodeJS 的main字段)
大致按以下步骤:
/// /root/src/moduleA.ts
import { b } from "./moduleB 复制代码
会如下面的流程来定位"./moduleB"
:
/root/src/moduleB.ts
/root/src/moduleB.tsx
/root/src/moduleB.d.ts
/root/src/moduleB/package.json (若是指定了"types"属性)
/root/src/moduleB/index.ts
/root/src/moduleB/index.tsx
/root/src/moduleB/index.d.ts
/// /root/src/moduleA.ts
import { b } from "moduleB"
复制代码
会如下面的流程来定位"./moduleB"
,流程与 node 解析相似,,只是会额外地从node_modules/@types
里寻找 d.ts
声明文件
/root/src/node_modules/moduleB.ts
/root/src/node_modules/moduleB.tsx
/root/src/node_modules/moduleB.d.ts
/root/src/node_modules/moduleB/package.json
(若是指定了"types"属性)
/root/src/node_modules/@types/moduleB.d.ts
/root/src/node_modules/moduleB/index.ts
/root/src/node_modules/moduleB/index.tsx
/root/src/node_modules/moduleB/index.d.ts
/root/node_modules/moduleB.ts
/root/node_modules/moduleB.tsx
/root/node_modules/moduleB.d.ts
/root/node_modules/moduleB/package.json
(若是指定了"types"属性)
/root/node_modules/@types/moduleB.d.ts
/root/node_modules/moduleB/index.ts
/root/node_modules/moduleB/index.tsx
/root/node_modules/moduleB/index.d.ts
/node_modules/moduleB.ts
/node_modules/moduleB.tsx
/node_modules/moduleB.d.ts
/node_modules/moduleB/package.json
(若是指定了"types"属性)
/node_modules/@types/moduleB.d.ts
/node_modules/moduleB/index.ts
/node_modules/moduleB/index.tsx
/node_modules/moduleB/index.d.ts
ts 构建时会把 .ts
编译成 .js
,并从不一样的源位置把依赖拷贝到同一个输出位置。所以,在运行时模块可能具备不一样于源文件的命名,或者编译时最后输出的模块路径与对应的源文件不匹配,为了不路径混乱的问题,ts 提供了一系列标记用来告知编译器指望发生在源路径上的转换,以生成最终输出
在tsconfig.json
能够指定 baseUrl
或 paths
来指导编译器查找须要导入的模块
{
"compilerOptions": {
"baseUrl": ".", // This must be specified if "paths" is.
"paths": {
"jquery": ["node_modules/jquery/dist/jquery"] // 此处映射是相对于"baseUrl"
}
}
}
复制代码
设置 baseUrl 告诉编译器到哪里去查找模块,全部动态查找模块的导入都会被当作相对于 baseUrl
ts 在解析动态查找模块时,是相对于 baseUrl 或根据 paths 寻找模块
baseUrl的值由如下二者之一决定:
tsconfig.json
里的 baseUrl 属性(相对路径的话,根据 tsconfig.json 所在目录计算)相对模块的导入不会被设置的 baseUrl 所影响,由于它们老是相对于导入它们的文件
paths
是相对于 baseUrl
进行解析。若是 "baseUrl"
被设置成了除 "."
外的其它值,好比 tsconfig.json 所在的目录,那么映射必需要作相应的改变。 若是你在上例中设置了 "baseUrl": "./src"
,那么jquery应该映射到"../node_modules/jquery/dist/jquery"
由于此时的基本路径为 /src/ ,而 node_modules 的目录实际上是在上一层目录
经过path
能够指定多个备选位置(也有翻译成 回退位置),好比下面的工程
/// 假设在一个工程配置里,有一些模块位于一处,而其它的则在另个的位置。 构建过程会将它们集中至一处
projectRoot
├── folder1
│ ├── file1.ts (imports 'folder1/file2' and 'folder2/file3')
│ └── file2.ts
├── generated
│ ├── folder1
│ └── folder2
│ └── file3.ts
└── tsconfig.json
复制代码
对应配置文件
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": [
"*",
"generated/*"
]
}
}
}
复制代码
这个配置的意思以下:
"*"
: 表示名字不发生改变,<moduleName>
路径映射为 <baseUrl>/<moduleName>
"generated/*"
: 表示模块名添加了 “generated” 前缀,<moduleName>
路径映射为<baseUrl>/generated/<moduleName>
在file1.ts
导入文件folder1/file2
和 folder2/file3
过程以下
folder1/file2
时
*
进行匹配
projectRoot/folder1/file2.ts
是否存在folder2/file3
时
*
进行匹配
projectRoot/folder2/file3
是否存在generated/*
进行匹配
projectRoot/generated/folder2/file3.ts
是否存在rootDirs
指定虚拟目录若是多个目录下的工程源文件在编译时合并在某个目录一并输出,这种行为能够认为是这些源目录建立了一个虚拟
目录,利用rootDirs
能够指定这个虚拟目录
好比有这样的工程
src
└── views
└── view1.ts (imports './template1')
└── view2.ts
generated
└── templates
└── views
└── template1.ts (imports './view2')
复制代码
这里假设构建工具会把它们整合到同一输出目录中(也就是说,运行时 view1 与 template1 是在一块儿的),把src/views
和 generated/templates/views
输出到同一个目录下
可使用rootDirs
指定一个roots列表,列表里的内容会在运行时被合并,针对上面的例子,能够这么设置
{
"compilerOptions": {
"rootDirs": [
"src/views",
"generated/templates/views"
]
}
}
复制代码
此后只要遇到指向 rootDirs
子目录的相对模块引入,都会尝试在 rootDirs
的每一项中查找,rootDirs
下的目录不要求必须存在
编译器在解析模块时可能访问当前文件夹外的文件,经过--traceResolution
启用编译器的模块解析追踪,能够查看模块在解析过程当中发生了什么。
--noResolve
--noResolve
编译选项告诉编译器,禁止添加任何文件(经过命令行传入的除外)。 此时编译器仍然会尝试解析模块,但再也不添加进来。
好比有这么一个文件
import * as A from "moduleA";
import * as B from "moduleB";
复制代码
进行这么编译
tsc app.ts moduleA.ts --noResolve
复制代码
能正确引入 moduleA,而对 moduleB 的引入则会报错找不到(假定 moduleA 和 moduleB 都正常存在)
默认状况下,tsconfig.json 所在目录即 TypeScript 项目目录,不指定files或exclude的话,该目录及其子孙目录下的全部文件都会被添加到编译过程当中。能够经过 exclude
选项排除某些文件(黑名单),或者用 files
选项指定想要编译的源文件(白名单)
编译过程当中,被引入的模块不管是否被 exclude 掉,都会被编译
声明合并
是指编译器将针对同一个名字的多个独立声明合并为单一声明,合并后的声明同时拥有原先多个声明的特性。
ts 中的声明会建立如下三种实体:命名空间、类型或值。
声明类型 | Namespace(命名空间) | Type(类) | Value(值) |
---|---|---|---|
Namespace | X | X | |
Class | X | X | |
Enum | X | X | |
Interface | X | ||
Type Alias | X | ||
Function | X | ||
Variable | X |
接口合并时非函数成员要求惟一,若是不是惟一的,要保证相同类型。若是不是,那么在编译的时候就会报错。
/// 成员惟一
interface Box {
height: number;
width: number;
}
interface Box {
scale: number;
}
let box: Box = {height: 5, width: 6, scale: 10};
...
/// 成员不惟一
interface Box {
color: string
}
interface Box {
color: number // 错误 后续属性声明必须属于同一类型.
}
复制代码
接口合并时函数成员若是同名,则会被看成同一函数的重载,后面出现的函数优先级更高
interface Cloner {
clone(animal: Animal): Animal;
}
interface Cloner {
clone(animal: Sheep): Sheep;
}
interface Cloner {
clone(animal: Dog): Dog;
clone(animal: Cat): Cat;
}
...
// 合并
interface Cloner {
clone(animal: Dog): Dog;
clone(animal: Cat): Cat;
clone(animal: Sheep): Sheep;
clone(animal: Animal): Animal;
}
复制代码
若是有一个参数的类型时单一的字符串字面量,会被提高到最顶端
interface IDocument {
createElement(tagName: any): Element;
}
interface IDocument {
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
}
interface IDocument {
createElement(tagName: string): HTMLElement;
createElement(tagName: "canvas"): HTMLCanvasElement;
}
...
// 合并
interface IDocument {
// 特殊签名置顶
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
createElement(tagName: "canvas"): HTMLCanvasElement;
// 下面两条仍遵循后声明的优先
createElement(tagName: string): HTMLElement;
createElement(tagName: "canvas"): HTMLCanvasElement;
}
复制代码
除了接口合并,其余的合并感受只会让代码混乱,暂时想不通为何会有那些合并
我的感受脱离 React 讨论 JSX 没啥意义,并且React的语法不停的更新,看相关教程中介绍的语法,已通过时
装饰器 是一种特殊类型的声明,它可以被附加到类声明,方法,访问符,属性或参数上。 装饰器使用 @expression
这种形式,expression
求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息作为参数传入
要启用实验性的装饰器特性,你必须在命令行或tsconfig.json里启用experimentalDecorators编译器选项
tsc --target ES5 --experimentalDecorators
复制代码
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
复制代码
装饰器工厂 就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。
咱们能够经过下面的方式来写一个装饰器工厂函数:
function color(value: string) { // 这是一个装饰器工厂
return function (target) { // 这是装饰器
// do something with "target" and "value"...
}
}
复制代码
类装饰器在类声明以前被声明,类装饰器应用于类的构造函数
,用来监视,修改或替换类定义,类装饰器不能用在声明文件中,也不用在任何外部上下文中
类装饰器表达式会在运行时看成函数被调用,类的构造函数
做为其惟一的参数。
若是类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。
/// constructor 是惟一参数
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greet = new Greeter("foo");
复制代码
@sealed
就是定义好的装饰器,这个装饰器能够阻止对构造函数和原型添加属性
方法装饰器在一个方法的声明以前,会被应用到方法的属性描述上,能够用来监视、修改或者替换方法的定义
方法装饰器表达式会在运行时看成函数调用,传入下列3个参数
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}
...
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
复制代码
@enumerable(false)
是一个装饰器工厂。 当装饰器 @enumerable(false)
被调用时,它会修改属性描述符的enumerable属性
访问器装饰器声明在一个访问器的声明以前(紧靠着访问器声明)。 访问器装饰器应用于访问器的 属性描述符而且能够用来监视,修改或替换一个访问器的定义。
TypeScript不容许同时装饰一个成员的get和set访问器
访问器装饰器表达式会在运行时看成函数被调用,传入下列3个参数:
若是访问器装饰器返回一个值,它会被用做方法的属性描述符
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() { return this._x; }
@configurable(false)
get y() { return this._y; }
}
...
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
复制代码
@configurable(false)
是一个访问器装饰器
属性装饰器声明在一个属性声明以前(紧靠着属性声明)
属性装饰器表达式会在运行时看成函数被调用,传入下列2个参数:
class Greeter {
@format("Hello, %s")
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
let formatString = getFormat(this, "greeting");
return formatString.replace("%s", this.greeting);
}
}
...
import "reflect-metadata";
const formatMetadataKey = Symbol("format");
function format(formatString: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
复制代码
@format("Hello, %s")
装饰器是个 装饰器工厂。 当 @format("Hello, %s")
被调用时,它添加一条这个属性的元数据,经过reflect-metadata
库里的Reflect.metadata
函数。 当 getFormat
被调用时,它读取格式的元数据。
参数装饰器声明在一个参数声明以前(紧靠着参数声明),参数装饰器应用于类构造函数或方法声明
参数装饰器表达式会在运行时看成函数被调用,传入下列3个参数:
参数装饰器的返回值会被忽略。
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@validate
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
}
...
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
let method = descriptor.value;
descriptor.value = function () {
let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
throw new Error("Missing required argument.");
}
}
}
return method.apply(this, arguments);
}
}
复制代码
@required
和 @validate
是装饰器
类中不一样声明上的装饰器按以下规定的顺序应用:
多个装饰器能够同时应用到一个声明上
@f @g x
复制代码
@f
@g
x
复制代码
当多个装饰器应用于一个声明上,它们求值方式与复合函数类似,ts 在编译时会进行以下步骤的操做:
function f() {
console.log("f(): evaluated");
return function(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log("f(): called");
};
}
function g() {
console.log("g(): evaluated");
return function(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log("g(): called");
};
}
class C {
@f()
@g()
method() {
console.log("C method()");
}
}
let c = new C();
c.method();
复制代码
调用结果以下
/// 先求值(按定义顺序,由上往下)
f(): evaluated
g(): evaluated
/// 依次调用(由下往上调用)
g(): called
f(): called
C method()
复制代码