TypeScript学习
众所周知,JavaScript并不是完美的,不care类型既是优点,也是缺点,虽然开发起来比较方便,但是有时候因为类型不确定找起来还是比较麻烦,以及缺少一些其他的约束规范,因此有TypeScript这个东西,微软开发的,从字面理解,最重要的功能就是加上了Type类型,能够给变量指定参数了。这个层面是指的是文件代码管理层面的,并非是实际运行的时候,也就是TypeScript是不能直接运行的(应该是这样子的,不排除可以直接运行,但是目前大部分是再重新转换成JavaScript文件再运行),单纯编写TypeScript的话,需要安装一下。
npm install -g typescript
然后运行的话,需要使用tsc命令把.ts结尾编译成.js的文件,再运行.js。
感觉是不是挺拉跨的,所以TypeScript提供了静态的代码分析,它可以分析代码结构和提供的类型注解,在编辑器里能直接报错,但tsc命令还是能够通过,并不影响相同语句的js文件运行,不过这就足够了。
在工程项目里,建议使用TypeScript,比如React和Vue3都在支持TypeScript。平时的刷算法可以直接使用JavaScript,毕竟那么短也不需要什么代码分析,算法才是关键。
因为TypeScript出来的比较早,那时候还没有ES6的语法,但是在TypeScript中实现了,这些ES6特性就不介绍了,毕竟JavaScript现在也差不多默认都支持ES6语法了,其中包括let、const、解构语法、class类。
放几个工具链接:
tsconfig.json
上来先讲tsconfig.json配置文件,有这个文件意味着这个目录是TypeScript项目的根目录,这个文件制定了用来编译这个项目的根文件和编译选项。建议放在根目录下,下面为一个示例
{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"**/*.spec.ts"
]
}
compilerOptions可以省略,省略会用默认值编译选项。这里挑几个用过的讲讲
-
target——指定ECMAScript目标版本
"ES3"(默认),"ES5","ES6"/"ES2015","ES2016","ES2017"或"ESNext"。 -
module——指定生成哪个模块系统代码,Node.js一般就是CommonJS吧,或者ES6
-
noImplicitAny——在表达式和声明上有隐含的
any类型报错 -
allowJs——允许编译JavaScript文件,一般配合checkJs一块使用
-
baseUrl——设置
baseUrl来告诉编译器到哪里去查找模块。 所有非相对模块导入都会被当做相对于baseUrl。baseurl的路径如果是相对路径,那么就是相对tsconfig.json文件。 -
paths——有些模块不是在baseUrl下的,所以用paths扩展,这里的路径是相当于baseUrl的路径。比如vue中,@/就是表示在src下的目录
"paths": { "@/*": ["src/*"] } -
declaration——生成对应的.d.ts文件
-
strict——启用所有严格类型检查选项,建议开启严格规范代码
-
moduleResolution——决定如何处理模块。或者是
"Node"对于Node.js/io.js,或者是"Classic"(默认) -
outDir——重定向输出目录
"files"指定一个包含相对或绝对文件路径的列表。 "include"和"exclude"属性指定一个文件glob匹配模式列表。 所以一般都用include。支持的glob通配符有:
-
*匹配0或多个字符(不包括目录分隔符) -
?匹配一个任意字符(不包括目录分隔符) -
**/递归匹配任意子目录
watch,表示一直监听
类型注解
这个最简单,直接给例子
let isDone: boolean = false; //布尔类型
let decLiteral: number = 6; //数字类型
let name: string = "bob"; //字符串类型
/* 数组类型 有两种方式,我都用 */
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];
/* 元组类型 不怎么常用,因为一般来说数组的类型基本上都是一致的 */
let x: [string, number];
x = ['hello', 10]; // OK
x = [10, 'hello']; // Error
/* 枚举类型,默认从0开始编号,也可以指定编号 */
enum Color {Red, Green, Blue} // enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green; // 此时c为0
//还可以反向映射,这个挺有用的,要注意的是 不会为字符串枚举成员生成反向映射。
let nameOfA = Color[c]; // "Red"
/* Any,这个最有用了,TypeSCript最终有可能变为AnyScript,哈哈 */
let list: any[] = [1, true, "free"];
let notSure: any = 4;
notSure.ifItExists(); // okay
/* Void类型,一般作为函数的返回值,是的,函数返回值也可以有类型,仿佛回到C++,go语言 */
function warnUser(): void {
console.log("This is my warning message");
}
/* Nerver类型,表示永远不存在的值,这个自己写的时候不会用到,但是有时候调用库的时候会有这个类型 */
function error(message: string): never {
throw new Error(message);
// 或者 return error("Something failed");
}
// 还有 undefined、null、symbol、Object这些不怎么常用的类型,不说了
/* 类型断言 没用过 */
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
// 或者用as
let strLength: number = (someValue as string).length;
接口
这其实是TypeScript第二常用的特性了,挺好用的,经常拿来指定对象属性的类型
interface LabelledValue {
label: string;
color?: string; // 问号表示此属性可选
readonly x: number; //表示这个值只能在刚创建的时候有,之后不能被修改,没用过
[propName: string]: any; // 如果不是上述的属性,那么就是any值,没用过
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
也可以用来指定函数的类型,比如传入的一个参数是函数,那么就可以指定了,参数名称并不需要和接口里的一样,好像C++也是这样子的?
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}
还有个索引签名,没见过也没用过,感觉很怪,不说了。
类类型,像Java一样,用implements,表示要实现的接口,这个接口只对实例部分进行类型检查,对于静态部分不检查,constructor不检查,反正我从来没用过。
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
接口也可以继承,也可以继承多个,也是比较有用的吧
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
混合类型,感觉这个比较乖啊,把函数类型以及对象属性一块使用了?有点牛的
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;
还有一个接口集成类?感觉就很迷,把类所有的成员继承过来,包括它的成员但不包括实现,这就不展示了,感觉有点脱裤子放屁。
类
class类这个es6就有了,继承也有了,所以不展示了。
公有私有,这个JavaScript好像还是提案?反正没有正式的支持,默认都是public,私有private,保护protected
class Animal {
public color: string;
private name: string;
constructor(theName: string) { this.name = theName; }
}
抽象类,这个是JavaScript没有在语法上支持的,在ES6中也没有吧。
abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法。在抽象类中定义的抽象方法,继承的子类必须要实现,普通方法可以不用去实现,而没有定义的抽象方法不能在子类中去定义,否则会报错。
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log('roaming the earch...');
}
}
函数
完整的函数类型,不过一般不这么写,因为实在是太长了
let myAdd: (x: number, y: number) => number =
function(x: number, y: number): number { return x + y; };
一般写成这样子,只留个参数类型,返回的类型靠他自己去推断,只要你返回的是一个明确类型的值,就能够推断出来。然后前面的类型也不用写了,因为他自己会判断出来的,能省则省,否则代码太长了
let myAdd = function (x: number, y: number) {
return x + y;
};
剩余参数的类型
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
为this显式的指定类型,提供一个显式的 this参数。 this参数是个假的参数,它出现在参数列表的最前面。注意这里createCardPicker里面的返回值用箭头函数, 箭头函数能保存函数创建时的 this值,而不是调用时的值。这里deck.createCardPicker()创建了函数,由于是对象的方法调用,所以createCardPicker里面的this值就是Deck类型的对象,箭头函数保存了这个值,所以里面的this也是这个值(箭头函数本身没有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);
还有函数重载,这个没用过,不说了
泛型函数,这个还能用一用。
// 定义泛型的函数
function identity<T>(arg: T): T {
return arg;
}
// 调用1
let output = identity<string>("myString");
// 或者根据类型推断
let output = identity("myString");
// 泛型约束,传入的类型必须具备length属性
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 padLeft(value: string, padding: string | number) {
// ...
}
类型断言?
interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(): Fish | Bird {
// ...
}
/* 为了让这段工作,可以用下面这个类型断言
let pet = getSmallPet();
// 每一个成员访问都会报错
if (pet.swim) {
pet.swim();
}
else if (pet.fly) {
pet.fly();
}
*/
let pet = getSmallPet();
// 类型断言
if ((<Fish>pet).swim) {
(<Fish>pet).swim();
}
else {
(<Bird>pet).fly();
}
或者用类型保护,pet is Fish就是类型谓词。 谓词为 parameterName is Type这种形式, parameterName必须是来自于当前函数签名里的一个参数名。
function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}
// 'swim' 和 'fly' 调用都没有问题了
if (isFish(pet)) {
pet.swim();
}
else {
pet.fly();
}
类型别名,和C++的很像
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === 'string') {
return n;
}
else {
return n();
}
}
type Container<T> = { value: T };
接口和类型别名的差别难以描述,反正能用接口就用接口,用不了接口再用类型别名。
索引类型和字符串索引签名
interface Map<T> {
[key: string]: T;
}
let keys: keyof Map<number>; // string
let value: Map<number>['foo']; // number
映射类型,这个看起来比较牛,好像在库中有见到过
比如想要对象中的每个属性都变成可选的。
type Partial<T> = {
[P in keyof T]?: T[P];
}
type PersonPartial = Partial<Person>;
或者对象中的每个属性都变成只读的
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
type ReadonlyPerson = Readonly<Person>;
模块解析
相对导入是以/,./或../开头的,其他的都是非相对的。
相对导入在解析时是相对于导入它的文件,并且不能解析为一个外部模块声明。 你应该为你自己写的模块使用相对导入,这样能确保它们在运行时的相对位置。
非相对模块的导入可以相对于baseUrl或通过下文会讲到的路径映射来进行解析。 它们还可以被解析成外部模块声明(.d.ts文件)。 使用非相对路径来导入你的外部依赖。
模块解析策略有两种:Node和Classic,AMD | System | ES2015时的默认值为Classic,其他情况默认为Node。
Classic主要是为了向后兼容,相对导入就正常去找.ts和.d.ts文件,非相对导入,就会从导入文件的目录一级一级往上找,指导根目录。
Node解析是参考了Node.js的文件解析,相对路径的话就去解析.ts,.tsx和.d.ts三个文件,按照文件名、文件夹名/package.json的types字段、文件夹名/index.ts来查找。讲的比较抽象,因为我懂了,因为简略一点。
命名空间
用namespace关键词来定义,放在namespace里面的只会在命名空间内使用,如果命名空间外的想要使用,需要使用export关键词在里面导出。
多个文件也可以使用一个命名空间,然后通过三斜线引入。插播三斜线:三斜线只能通过在文件的开头出现,前面只能出现单行或者多行注释,不能出现语句。三斜线引用告诉编译器在编译过程中要引入的额外的文件。
/// <reference path="***.ts" />
写.d.ts需要使用引入其他类型的文件,可以使用下面的语句
/// <reference types="..." />
/// <reference types="node" />
表示这个文件引用了@types/node/index.d.ts里面声明的名字
不要包含默认的库,比如lib.d.ts
/// <reference no-default-lib="true"/>
ok,插播完毕,回到命名空间。
由于每次使用命名空间导出的东西,都需要命名空间.东西这样子写法,可以使用import来取别名,注意别和import xxx = require() 模块混在一起,虽然长得很像。
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()"
命名空间和模块的区别,模块支持声明它的依赖,模块会把依赖添加到模块加载器。所以在我看来,模块吊打命名空间,没有什么必要使用命名空间。模块和命名空间只能二选一,所以选择模块。
声明文件
有些库他是JavaScript的库,并不能直接使用,需要导入相应的types库,或者写一个声明文件,也就是.d.ts后缀的文件。
全局变量的声明,使用declare var声明变量。 如果变量是只读的,那么可以使用 declare const。 你还可以使用 declare let如果变量拥有块级作用域。
declare var foo: number;
全局函数,使用declare function声明函数。
declare function greet(greeting: string): void;
带属性的对象,全局变量myLib包含一个makeGreeting函数, 还有一个属性 numberOfGreetings指示目前为止欢迎数量。
let result = myLib.makeGreeting("hello, world");
console.log("The computed greeting is:" + result);
let count = myLib.numberOfGreetings;
使用declare namespace描述用点表示法访问的类型或值。
declare namespace myLib {
function makeGreeting(s: string): string;
let numberOfGreetings: number;
}
函数重载,getWidget函数接收一个数字,返回一个组件,或接收一个字符串并返回一个组件数组。
declare function getWidget(n: number): Widget;
declare function getWidget(s: string): Widget[];
接口
当指定一个欢迎词时,你必须传入一个
GreetingSettings对象。 这个对象具有以下几个属性:1- greeting:必需的字符串
2- duration: 可靠的时长(毫秒表示)
3- color: 可选字符串,比如‘#ff00ff’
interface GreetingSettings {
greeting: string;
duration?: number;
color?: string;
}
declare function greet(setting: GreetingSettings): void;
经常使用的就是扩展window了
declare interface Window {
log: any;
writeFileSync: any;
test: any;
api: any;
}
类型别名
function getGreeting() {
return "howdy";
}
class MyGreeter extends Greeter { }
greet("hello");
greet(getGreeting);
greet(new MyGreeter());
你可以使用类型别名来定义类型的短名:
type GreetingLike = string | (() => string) | MyGreeter;
declare function greet(g: GreetingLike): void;
组织类型
文档
greeter对象能够记录到文件或显示一个警告。 你可以为.log(...)提供LogOptions和为.alert(...)提供选项。
代码
const g = new Greeter("Hello");
g.log({ verbose: true });
g.alert({ modal: false, title: "Current Greeting" });
声明
使用命名空间组织类型。
declare namespace GreetingLib {
interface LogOptions {
verbose?: boolean;
}
interface AlertOptions {
modal: boolean;
title?: string;
color?: string;
}
}
类
文档
你可以通过实例化
Greeter对象来创建欢迎词,或者继承Greeter对象来自定义欢迎词。
代码
const myGreeter = new Greeter("hello, world");
myGreeter.greeting = "howdy";
myGreeter.showGreeting();
class SpecialGreeter extends Greeter {
constructor() {
super("Very special greetings");
}
}
声明
使用declare class描述一个类或像类一样的对象。 类可以有属性和方法,就和构造函数一样。
declare class Greeter {
constructor(greeting: string);
greeting: string;
showGreeting(): void;
}
外部模块
declare module
declare module "url" {
export interface Url {
protocol?: string;
hostname?: string;
pathname?: string;
}
export function parse(urlStr: string, parseQueryString?, slashesDenoteHost?): Url;
}
declare module "path" {
export function normalize(p: string): string;
export function join(...paths: any[]): string;
export let sep: string;
}
然后使用
/// <reference path="node.d.ts"/>
import * as URL from "url";
let myUrl = URL.parse("http://www.typescriptlang.org");
简写,快速使用,声明一下这个模块就行了,导出的就都是any类型了,这个最常用了,哈哈哈。
declare module "hot-new-module";





