TypeScript特性
- JavaScript的一个超集,微软开发并开源,任何现有的JavaScript 程序可以不加改变的在 TypeScript 下工作。使用typescript编写的代码可以被编译成为符合ES5、ES6标准的JavaScript
1、ES6的新语法,下一代JavaScript的标准:
规范模块化
Class语法
let、const
箭头函数
async函数
Set、Map
2、ES2019引入的新语法:
2.1 可选链运算符
obj?.prop
obj?.[expr] 短路求值 : obj为undefined/null 不求值
fn?.()
2.2 空值合并运算
a??b 逻辑操作符
当左侧的操作数为undefined/null时,返回其右侧操作数,否则返回左侧操作数
逻辑或操作符(||)不同,逻辑或操作符会在左侧操作数为假值时返回右侧操作数。【假值:null 或者 undefined+0+false】
- 除了支持所有JavaScript的新语法,还提供了静态类型检查。静态类型检查在大型项目中优势突出,大型项目的多个模块在对接时候,静态类型检查可以在编译阶段就找出可能存在的问题
@ts-ignore注释指令,禁止对下一行代码的类型检查
- TypeScript未经编译无法直接运行在浏览器和node环境。
当我们编译目标是ES5,但是同时又想要使用ES6、ESNext和其他运行环境时,使用lib进行扩展
//tsconfig.json
"compilerOptions":{
"target":"es5",
"lib":["es5","dom","scripthost","es2015.symbol"]
}
类型系统
- TypeScript使用的是结构性类型系统 比较两种不同的类型时,只要所有成员的类型都是兼容的,就认为它们的类型是兼容的,可以赋值。
- 参数属性会给构造函数参数添加一个访问限定符来声明变量作用域 e.g:使用private限定一个参数属性会声明并初始化一个私有成员
注意带有private或protected成员的类型约束规则,参数属性限定了作用域,所以只有当两个类型都包含该private成员且它们都是来自同一类的声明时,才认为这两个类型是兼容的。
- 区别类和类的构造函数,前者可以理解为一个特殊的“作用域”,后者则是位于“作用域”中的实例化一个类时会调用的钩子函数
- 区别类型约束 :类 : typeof 类
typeof GreeterClass,取GreeterClass类的类型,即构造函数的类型而不是实例的类型。这个类型包含了类的所有静态成员和构造函数。
修饰符(即参数属性)
只读属性:const / readonly
- 变量使用const
- 属性使用readonly
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
readonly只读属性必须在声明时或构造函数里被初始化
底层实现:只带有get不带有set的存取器
public
在构造函数的参数上使用public等同于创建成员变量。
class Student {
fullName: string;
constructor(public firstName: string, public middleInitial: string, public lastName: string) {
this.fullName = firstName + " " + middleInitial + " " + lastName;
}
}
在TypeScript里,成员都默认为public,当成员被标记成private时,不能在声明它的类的外部访问,派生类也无法访问
private
class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;//只能在申明的类的内部访问
}
}
new Animal("Cat").name; // 错误: 'name' 是私有的.
private主要使用:内部的无需直接对外暴露的某个字段
let passcode = "secret passcode";
class Employee {
private _fullName: string;
get fullName(): string {//暴露private的方法
return this._fullName;
}
set fullName(newName: string) {
if (passcode && passcode == "secret passcode") {
this._fullName = newName;
}
else {
console.log("Error: Unauthorized update of employee!");
}
}
}
let employee = new Employee();
employee.fullName = "Bob Smith";//实现访问
if (employee.fullName) {
alert(employee.fullName);
}
protected
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() {
//派生类中可以访问protected
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); // 错误
tricky: protected构造函数实现抽象类
将构造函数标记成protected,意味着这个类不能在包含它的类外被实例化(调用构造函数),但是能被继承。
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' 的构造函数是被保护的.
声明空间
注意区别两种声明空间,兼有或者独有
- 类型声明空间,可以用来类型注解
- 变量声明空间,声明作用域、生命形态
- 用var、const、let声明的变量,没有在类型声明空间,只能在变量声明空间中使用,不能用作类型注解。
class F{}
interface B{}
type C={}
const foo=1;
let bar:foo;//错误
- interface B不能当作变量使用,因为interface没有定义在变量声明空间
声明合并
- 同名接口自动合并
- 同名的枚举自动合并
- 同名namespace自动合并
- 不支持同名类合并,但是外部类声明可以与同名的接口声明合并
声明文件列表
- node_modules/@types是一个特殊的目录,ts将其视作第三方声明文件的根目录,编译时自动将声明文件添加到编译文件列表。在安装DefinitelyTyped提供的声明文件时,它会被安装到该目录下。
- 在编译文件中使用typeRoots和types编译选项设置。前者是目录,后者是具体的声明文件
interface
interface可以理解为一个特殊的类,关于其使用主要围绕以下两个方面展开
- type特性,做类型注解、继承
- 接口特性,implement对类的实现加以约束
interface做类型注解
下面针对-类型注解目标的不同分别展开:
一、interface-对象的类型注解
类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对 只要包含一个label属性且类型为string的对象即可
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
可选属性: 一旦使用,引入严格检查-捕获引用了不存在的属性时的错误
interface SquareConfig {
color?: string;
width?: number;
}
多余属性
- 可选属性之外还有其他未知属性
- 原理:若目标对象的类型约束上存在索引签名,那么目标对象可以接收任意属性
- 注意 字符串索引签名会约束该对象类型中所有属性的类型。即所有属性的类型必须可以赋值给 字符串索引的类型,包括数字索引签名。因为所有索引签名会隐式调用toString方法
[propName: string]: any;
补充:suppressExcessPropertyErrors 编译选项,禁用整个工程的多余属性检查
可索引类型
- 索引签名:string、number
- 同时使用两种类型的索引时候:当使用number来索引时,JavaScript会将它转换成string然后再去索引对象,因此string索引的值约束对所有key-value生效。数字索引的value必须是字符串索引的value的子类型。
- 给索引签名添加readonly设置为只读,初始换完成后不能给索引赋值,注意是不能使用-采用索引获得的元素来赋值,而不是整体赋值。
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let my: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
my=["ss","hjhj"];//可以成功
二、interface-函数的类型注解
interface SearchFunc {
(source: string, subString: string): boolean;
}
三、interface-混合类型的类型注解
例如:一个对象可以同时做为函数和对象使用,并带有额外的属性。本质就是同名方法与同名对象,然后返回 同名
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = <Counter>function (start: number) { };//同名方法
counter.interval = 123;//定义对象属性
counter.reset = function () { };
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
类型断言:等号右边加<type>,等同as type
上列代码中是对函数返回值的类型断言还是函数本身?
- 注意写法,这里
<type>后面跟的是函数声明,所以是对函数。 - 如果是函数调用,就是对返回值的类型约束
在使用JavaScript第三方库的时候,可能需要像上面那样去完整地定义类型。
四、特殊:构造器接口
构造器接口不能implement,应该直接约束类的静态部分
interface ClockConstructor {//构造器接口,注意不implement,直接使用
new (hour: number, minute: number): ClockInterface;//用一个interface限制构造器返回值
}
interface ClockInterface {
tick();//
// some();接口这里定义的方法,每一个都要实现
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);//函数连接多个接口
}
class DigitalClock implements ClockInterface {//实现 限制构造器生成结果的interface
constructor(h: number, m: number) {}
tick() {
console.log("beep beep");//定义在DigitalClock.prototype上
}
}
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);
interface implement
接口描述了类的公共 public部分,不会检查类的私有 private成员。区别于接口继承类
类是具有两个部分的:static部分的类型和实例的类型,constructor属于类的静态部分。当一个类实现了一个接口时,即implement,只对其实例部分进行类型检查
interface ClockConstructor {
new (hour: number, minute: number);//error
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
抽象&接口
注意:abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法。不同于接口,抽象类可以包含成员的实现细节。
类都可以用作类型约束, abstract 类也不例外,这时候要求abstract类与子类的成员是等集
abstract class Department {
constructor(public name: string) {
}
printName(): void { // 实现细节
console.log('Department name: ' + this.name);
}
abstract printMeeting(): void; // 必须在派生类中实现
}
接口继承extends接口、类
一、单继承、多继承
接口可以继承,单继承、多继承 与接口合并有区别吗
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;
二、接口继承一个类:只能被类实现
类定义会创建两个东西:类的实例类型和静态类型(默认包含一个构造函数)。 因为类可以创建出类型,所以允许使用接口的地方使用类。
会继承到类的private和protected成员但不包括其实现, 这意味一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control implements SelectableControl {
select() {
//console.log(this.state);//error只有在Control内部可以访问
//在Control类内部,是允许通过SelectableControl的实例来访问私有成员state的。
}
}
// Error: Property 'state' is missing in type 'Image'.
class Image implements SelectableControl {
private state: any;//还是错误的,必须是声明于Control的私有成员state
select() { }
}
函数
函数类型包含两部分:参数类型和返回值类型
函数参数
相比接口的类型检查,函数参数的类型检查是通过位置匹配的,不需要名字相同
//注意这里是getRender(),是对函数返回值的类型检查
function getRender(): (vnode: JSX.Element | null, context?: {
contextTypes: Object;
value: Object;
} | undefined) => void;
//注意这里是getRender,是对函数的类型检查
const getRender: () => (vnode: JSX.Element | null, context?: {
contextTypes: Object;
value: Object;
} | undefined) => void;
REST处理可变数量的参数,REST参数是一个数组
function f(first:number,...rest:string[]){}
可选参数
- TypeScript里在参数名旁使用?实现可选参数,可选参数必须跟在必须参数后面
- 在“--strictNullChecks”模式下,typescript自动为每个可选参数添加一个undefined类型
(firstName: string, lastName?: string) => string
-
带默认初始化的参数属于可选参数的,共享 【?可选参数 】的类型判断
-
与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面。 如果带默认值的参数出现在必须参数前面,用户必须明确的传入undefined值来获得默认值。
回调函数里的this参数的类型检查
ts支持在函数参数列表中,通过定义this参数来描述函数中的this值的类型,可以实现指定调用对象
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
//改用箭头函数
//return () => {
return function() {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
//注意这里的this.suits只在当前词法作用域存在,需要使用箭头函数才能保证绑定词法作用域
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
noImplicitThis编译选项 TypeScript会警告你犯了一个错误,如果你给编译器设置了--noImplicitThis标记。 它会指出this.suits[pickedSuit]里的this的类型为any。
提供一个显式的this参数。 this参数是个假的参数,它出现在参数列表的最前面:
interface Card {
suit: string;
card: number;
}
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// NOTE: The function now explicitly specifies that its callee must be of type Deck
createCardPicker: function(this: Deck) {
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
现在TypeScript知道createCardPicker期望在某个Deck对象上调用。 也就是说this是Deck类型的,而非any,因此--noImplicitThis不会报错了。
class Handler {
info: string;
onClickGood = (e: Event) => { this.info = e.message }
}
箭头函数的类型约束
f=(agrs: string): returnTypeInterface | undefined => {}
函数签名
- 重载函数是指一个函数同时拥有多个同类的函数签名
- 多条函数重载语句(不带函数体)+一条函数实现语句
- 函数重载必须在函数实现之前,且之间不能出现其他语句
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);
函数是一种对象,可以拥有自己的属性 version属性——用来标记重载函数
泛型:就像变量x
<>
一个无限可能的类型,就像变量x,等实例化、实际调用函数时候再明确它的定义。
返回与传入的参数类型是相同:
function identity<T>(arg: T): T {
//只能调用通用类型都有的方法,数字不支持length
return arg;
}
//调用泛型
let output = identity<string>("myString");
let output1 = identity("myString");
解决length的方法,还不太懂实际运用,另一种解决看后面-泛型约束
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
泛型函数、接口
TypeScript编译器不允许展开泛型函数上的类型参数。 这个特性会在TypeScript的未来版本中考虑实现。
关于泛型函数的三种书写风格
- 泛型 箭头函数约束
- 泛型 对象字面量约束(函数的本质也是对象)
- 泛型 接口(特殊的语义化的对象)
function identity<T>(arg: T): T {
return arg;
}
// 箭头函数
let myIdentity: <T>(arg: T) => T = identity;
let myIdentity: <U>(arg: U) => U = identity;
//使用带有调用签名的对象字面量来定义泛型函数
let myIdentity: {<T>(arg: T): T} = identity;
// 泛型接口
interface GenericIdentityFn {
<T>(arg: T): T;
}
let myIdentity: GenericIdentityFn = identity;
//优化,把泛型参数当作整个接口的一个参数
interface GenericIdentityFn<T> {
(arg: T): T;
}
let myIdentity: GenericIdentityFn<number> = identity;
想把泛型参数当作整个接口的一个参数,清楚的知道使用的具体是哪个泛型类型(比如:Dictionary<string>而不只是Dictionary),接口里的其它成员也能知道这个参数的类型。
可以创建泛型类、泛型接口 。 注意,无法创建泛型枚举和泛型命名空间。
泛型类
类有两部分:静态部分和实例部分,没有必要约束共有的静态部分,所以当我们说了class的类型系统的时候,约束的一直是实例部分。
所以类的静态属性不会也不能使用这个泛型类型。
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; };
举个例子
符合约束类型的值,必须包含必须的属性
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;
}
可以声明一个类型参数,且它被另一个类型参数所约束。
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'.
创建工厂函数
function create<T>(c: {new(): T; }): T {
return new c();
}
使用 extends 约束构造函数与类实例的关系
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!
TypeScript具有ReadonlyArray<T>类型,它与Array<T>相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:
原始类型
除了对JavaScript中的原始类型Boolean、number、string、bigint、symbol、undefined、null做出了实现和细分,还新增了void、枚举类型、字面量类型、交叉类型。
单元(单例)类型:仅包含一个可能值的类型 有null类型、unique symbol、void、字面量、联合枚举成员类型 联合类型注解:多种类型中的一种,用“|”分离 类型别名:
type StrOrNum=string |number;
let sample:StrOrNum;
type Coordingates=[number,number];
type Callback=(data:number)=>void;
数组
新增数组泛型,本质是对array构造函数使用泛型
let a: number[];
let a: Array<number>
只读数组
ReadonlyArray<T>
Readonly<T>
使用修饰符readonly
元组类型
数组类型的子类,是长度固定的数组,并且元组中每个元素都有确定的类型。也有例外,可选元素 ?
let tru:[string,number];
当访问一个越界的元素,会使用界内元素类型的联合类型检查替代
只读元组
readonly
Readonly<T>
联系解构语法
// swap variables
[first, second] = [second, first];
// 只取对应解构部分的值
let [first] = [1, 2, 3, 4];
console.log(first); // outputs 1
补充 对象解构 也可以用...获取剩余属性
let o = {
a: "foo",
b: 12,
c: "bar"
};
let { a, b } = o;
注意对象展开,仅保留其自身的可枚举属性,即丢失其方法
symbol与unique symbol
符号的初始化
Symbol();
Symbol.for();
unique symbol作为一种变通方法,让symbol有字面量的性质,即仅表示一个固定的值 只允许用const、readonly声明定义unique symbol
const a:unique symbol=Symbol();
const a:unique symbol=Symbol.for("same");
注意以下,a、b会引用同一个Symbol,但是TS编译器无法识别出这种错误
const a:unique symbol=Symbol.for("same");
const b:unique symbol=Symbol.for("same");
void
用作函数的返回值类型
function f(mes:string): void{
console.log(h);
}
声明一个void类型的变量,只能为它赋予undefined和null
void 作为运算符存在于 JavaScript 中,而作为基本类型存在于 TypeScript 中。 void 总是计算它旁边的表达式,同时总是返回 undefined,
// 从函数返回而不返回一个值,但仍然调用一个回调
function middleware(nextCallback) {
if(conditionApplies()) {
return void nextCallback();
}
}
TypeScript 中的 void 是 undefined 的子类型。JavaScript 中的函数总是返回一些东西。要么它是一个值,要么是 undefined 因为没有返回值的函数总是返回 undefined,而 void 总是在 JavaScript 中返回 undefined,TypeScript 中的void 是一个正确的类型,告诉开发人员这个函数返回 undefined
function doSomething(callback: () => void) {console.log(callback());}
function aNumberCallback(): number { return 2; }
doSomething(aNumberCallback)
注意上文如果将类型约束替换为下面,会报错
function doSomething(callback: () => undefined) { console.log(callback());}
null与undefined(注意strictNullChecks)
默认TypeScript把null和undefined当做属于任何类型,即默认是所有类型的子类型,声明为number类型的值可以为null和undefined。 当启用了strictNullChecks,null和undefined获得了它们自己各自的类型null和undefined。这时候想要实现任何值可能为null,需要使用联合类型。 e.g:某值可能为number或null,声明它的类型为number | null。
枚举(关注:引用枚举)
类型约束上:可以视作联合类型的另一种解决方案
根据枚举成员的引用关系可以划分为三类
- 常数枚举
- 引用枚举
- 外部枚举
常数枚举是在enum关键字前使用const修饰符。 常数枚举只能使用常数枚举表达式, 常数枚举成员在使用的地方被内联进来。 这是因为常数枚举不可能有计算成员。
引用枚举成员总会生成一次属性访问并且永远不会内联。 在大多数情况下这是很好的并且正确的解决方案。 然而有时候需求却比较严格。 当访问枚举值时,为了避免生成多余的代码和间接引用,可以使用常数枚举。
外部枚举(加关键字declare)用来描述已经存在的枚举类型的形状。
外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常数成员。 对于非常数的外部枚举而言,没有初始化方法时被当做需要经过计算的。
根据枚举成员的类型可以划分为三类
- 数值型枚举
- 字符串枚举
- 异构型枚举
数值型枚举 也可以手动给所有枚举类型编号
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
//枚举映射 专属数值型枚举的隐射
enum Color {Red = 1, Green, Blue}//从1开始编号
let co:number=Color.Red;//1
let colorName: string = Color[2];//Green
//等同于
let colorName: Color =2;
alert(colorName); // 显示'Green'因为上面代码里它的值是2
异构型枚举
enum D{
up="up",
black=1,//跟在字符串枚举成员后面必须指定一个初始值
}
只写key不写value===》value=key,字符型枚举?
never类型(最幼)
是任何类型的子类型,但是自己没有子类型
// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
while (true) {
}
}
function error(message: string): never {
throw new Error(message);
}
// 推断的返回值类型为never
function fail() {
return error("Something failed");
}
总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型
字面量类型
支持字面量作为类型使用
可以参看前面 函数泛型的代码
顶端类型any与unknown(最老)
any 所有类型的父类型,在any类型上允许执行任何操作而不会产生编译错误。也是缺少类型注解时候的默认值,即隐式的any类型。
//tsconfig.json
"noImplicitAny": true
any与类型断言 类型断言语法有as或者<>
let qq=1;
let q="wws";
q=qq as any;//正确
q=<any>qq;
q=qq;//error
object类型允许赋值所有类型,但是并不能调用该类型的方法,即使该类型真的存在该方法 ?object的用处 当我们申明类型检查之后,我们需要将数据当作该类型处理。
unknown:类型安全的any,在使用之前必须将其细化为某种具体类型。优先使用
let x:any;//x可以赋值给除了never之外的所有类型,null、undefined也可以
let w:unknown;//w只能赋值给unknown、any
所有类型都能赋值给any和unknown
交叉类型&
通过联合类型的对立面来理解 使用extend模式,根据两个对象创建一个新对象
function extend<T, U>(first: T, second: U): T & U {
let result = <T & U>{};
for (let id in first) {
(<any>result)[id] = (<any>first)[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<any>result)[id] = (<any>second)[id];
}
}
return result;
}
class Person {
constructor(public name: string) { }
}
interface Loggable {
log(): void;
}
class ConsoleLogger implements Loggable {
log() {
// ...
}
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();
联合类型 |
类型别名 type
类型约束
通过关键字 extend 进行约束,不同于在 class 后使用 extends 的继承作用,泛型内使用的主要作用是对泛型加以约束
类型索引 keyof
类似于 Object.keys ,用于获取一个接口中 Key 的联合类型
映射类型
通过 in 关键字做类型的映射,遍历已有接口的 key 或者是遍历联合类型 keyof T:通过类型索引 keyof 的得到联合类型 'a' | 'b' P in keyof T 等同于 p in 'a' | 'b',相当于执行了一次 forEach 的逻辑,遍历 'a' | 'b'
条件类型
条件类型的语法规则和三元表达式一致,经常用于一些类型不确定的情况。 T extends U ? X : Y 上面的意思就是,如果 T 是 U 的子集,就是类型 X,否则为类型 Y
其他
常用关键字
类型断言
尖括号、as
?区别
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
即使是把整个ReadonlyArray赋值到一个普通数组也是error,需要使用类型断言
存取器
TypeScript支持通过getters/setters来截取对对象成员的访问。
let passcode = "secret passcode";
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (passcode && passcode == "secret passcode") {
this._fullName = newName;
}
else {
console.log("Error: Unauthorized update of employee!");
}
}
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
alert(employee.fullName);
}
JSDOC
根据javascript文件中注释信息,生成JavaScript应用程序或库、模块的API文档 的工具。 提供了很多标签,对应实现许多功能
@typeof创建自定义类型
实现类声明空间
/**
*@typeof{(number|string)} NumberLike
*/
@type定义变量类型 类型检查
/**
*@type{string)}
*/
let a;
@param定义函数参数类型
@return、@returns定义函数返回值类型
@extends定义继承的基类
@public、@protected、@private
装饰器(实验性能)
修饰器只能用于类和类的方法,不能用于函数,因为存在函数提升
装饰器,返回另一个函数的函数,在程序首次运行时执行。
- 使用这些装饰器函数来包装需要装饰后的代码,并将包装后的代码作为返回值。通过具体的代码逻辑,装饰器可以实现监视,修改或替换。不同类型(位置决定)的修饰器返回的函数可以接收到的参数不同
- 在当前的浏览器或Node.js版本中,尚不支持装饰器,使用Babel插件@babel/plugin-proposal-decorators来将其转换为浏览器或Node.js中可识别的代码。
类中不同声明上的装饰器将按以下规定的顺序应用:
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
- 参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
- 参数装饰器应用到构造函数。
- 类装饰器应用到类。
装饰器不能用在声明文件中( .d.ts),也不能用在任何外部上下文中(比如declare的类)
定制一个修饰器如何应用到一个声明上,写一个装饰器工厂函数(由上而下被调用),它返回一个表达式,以供装饰器在运行时调用(由下而上被调用)。
装饰器 若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json里启用experimentalDecorators编译器选项: 命令行:
tsc --target ES5 --experimentalDecorators
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
装饰器是一种特殊类型的声明,被附加到类声明,方法, 访问符,属性或参数上。
多个装饰器的调用顺序
可以把下列代码赋值到 ts学习网站运行看看效果
注意直接打印object不能取到暂时的值,object是引用,console.log(JSON.stringify(target));取到当时的值
function first() {
console.log("first(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("first(): called");
target.firstADD = "first do";
console.log(JSON.stringify(target));
console.log(propertyKey);
}
}
function second() {
console.log("second(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("second(): called");
target.secondADD = "second do";
console.log(JSON.stringify(target));
console.log(propertyKey);
}
}
class C {
static id = 0;
@first()
@second()
method() {console.log("类的成员方法被调用"); }
}
console.log("开始执行");
var c = new C();
console.log(c);
c.method();
结合这个效果就很显然了,可以试着去掉执行语句,感受装饰器实际执行的时间
函数装饰器、类装饰器在被装饰的函数定义之后立即运行,返回值会被计算。如果装饰器函数调用了被装饰的函数,那么被装饰的函数会被执行。 注意:return func()与return func 函数装饰器在被装饰的函数定义之后立即运行。 被装饰的函数只在明确调用时运行。 装饰器返回的应该是函数调用func而不是函数的运行结果func() 一般返回的结果是函数,由下至上依次调用,每次调用接收到的参数是下面函数的执行结果
类装饰器
类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。
构造器的类型检查
<T extends {new(...args:any[]):{}}>
function classDecorator<T extends {new(...args:any[]):{}}>
(constructor:T) {
return class extends constructor {
newProperty = "new property";
hello = "override";
}
}
@classDecorator
class Greeter {
property = "property";
hello: string;
constructor(m: string) {
this.hello = m;
}
}
console.log(new Greeter("world"));
方法装饰器
方法装饰器表达式会在运行时当作函数被调用
传入下列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;
};
}
访问器装饰器
访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。 访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。
TypeScript不允许同时装饰一个成员的get和set访问器。 一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。
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;
};
}
属性装饰器
属性装饰器声明在一个属性声明之前(紧靠着属性声明)
属性描述符不会做为参数传入属性装饰器,这与TypeScript是如何初始化属性装饰器的有关。 因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。因此,属性描述符只能用来监视类中是否声明了某个名字的属性。
属性装饰器表达式会在运行时当作函数被调用,传入下列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);
}
参数装饰器
参数装饰器只能用来监视一个方法的参数是否被传入。 参数装饰器声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。
参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。 成员的名字。 参数在函数参数列表中的索引。
@required装饰器添加了元数据实体把参数标记为必需的。 @validate装饰器把greet方法包裹在一个函数里在调用原先的函数前验证函数参数。
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);
}
}
工程建构相关
编译空间
TypeScript项目的根目录下存在一个tsconfig.json文件,文件中指定了用来编译这个项目的根文件和编译选项。
一个项目可以通过以下方式之一来编译: 当命令行上指定了输入文件时,tsconfig.json文件会被忽略。 不带任何输入文件的情况下调用tsc,编译器会从当前目录开始去查找tsconfig.json文件,逐级向上搜索父目录。 不带任何输入文件的情况下调用tsc,且使用命令行参数--project(或-p)指定一个包含tsconfig.json文件的目录。
ts编译器:位于lib文件夹下面,对外提供tsc命令支持编译 tsconfig.json文件的书写 tsconfig.json文件可以利用extends属性从另一个配置文件里继承配置。
{
"extends": "./configs/base",
"files": [
"main.ts",
"supplemental.ts"
],
"references":[/**对象数组,设置引用的工程**/
{"path":"../pkg1"},
{"path":"../pkg2"},
]
}
指定编译对象
- "files"指定一个包含相对或绝对文件路径的列表。顺序编译,可以代表依赖关系(依赖关系也可以在各自的文件中用///定义自己的依赖)
- "include"和"exclude"属性指定一个文件glob匹配模式列表。 支持的glob通配符有:
匹配0或多个字符(不包括目录分隔符)* 匹配一个任意字符(不包括目录分隔符)? 递归匹配任意子目录 **/
如果"files"和"include"都没有被指定,编译器默认包含当前目录和子目录下所有的TypeScript文件(.ts, .d.ts 和 .tsx)
模块
当文件中含有import、export会在当前文件中创建一个本地的作用域即文件(外部)模块,如没有就被称作“脚本”,是全局模块,直接存在于全局作用域
一个带有外部模块的TypeScript文件会生成什么样的JavaScript文件,由编译选项module决定
导入时候,除了静态查找模块路径,当导入路径不是相对路径时候采用动态查找
import * as foo from 'foo'
动态查找顺序如下:与Node模块解析策略相似
./node_modules/foo ../node_modules/foo ../../node_modules/foo 一直查到系统根目录
如果导入模块没有被当作变量声明空间来使用,那么在编译成JavaScript时,导入会被完全移除。 那如果我打算引入一个模块做类型声明空间来使用怎么办?
//基于COMMJS的代码
import hello=require('./hello');
export function loadF(){
let _hello: typeof hello;
}
只导出类型,不导出具体的值。即对应只使用他的类型声明空间
import type { Point } from “./util”
动态导入 按需加载,import函数实现,该函数返回一个promise对象
外部模块 declare在文件顶部声明,作用在全局命名空间
tsconfig.json中paths指定模块的隐射路径