快速上手TypeScript
TypeScript基础知识梳理
之前一直打算看看ts,但一直沉迷摸🐟无法自拔,公司目前项目中也没有ts项目。今年手上正好有两个小程序项目,于是打算用ts写一下,感受一下ts的“魅力”。顺便整理了一下学习笔记,希望能帮到有需要的人。同时也用XMind做了知识图谱,有需要源文件的可以联系我。
安装与编译
安装
npm install -g typescript //mac下安装前面需要加sudo
编译
- 可以执行下面命令进行编译,会在当前目录下产生一个
test.js
文件
tsc test.ts
- 这样可能还是比较麻烦,我们可以借助插件方便我们提高效率:
npm install -g ts-node
- 安装完后只需执行
ts-node test.ts
就可以在控制台查看输出结果。
原始数据类型
- js中数据类型分两种,原始数据类型和对象类型,原始类型包括:布尔值、数字、字符串、null、undefined以及Symbol。
布尔值
- 在TypeScript中,使用boolean定义布尔值类型
let isStatus:boolean=true;
数字
- 使用number定义数值类型
let isNumber:number=1;
//16进制(编译后显示10进制数字)
let isNumber16:number:=0b1010;
字符串
- 使用string定义字符串类型:
let isString:string='hello word';
let year:number=2020;
let add:string=`${isString},${year}!`;
空值
- JavaScript 没有空值(Void)的概念,在TypeScript中,可以用void表示没有任何返回值的函数.
function alertName():void{
alert("my name is tom")
}
- 声明一个 void 类型的变量没有什么用,因为你只能将它赋值为 undefined 和 null。
let unusable: void = undefined;
Null 和 Undefined
- 在TypeScript中可以使用null和undefined来定义这两个原始数据类型
let u:undefined=undefined;
let n:null=null;
- 与 void 的区别是,undefined 和 null 是所有类型的子类型。也就是说undefined类型的变量,可以赋值给number类型的变量,而void类型的变量不能赋值给number类型的变量:
let num:number=undefined;//不会报错
let u:undefined;
let num: number = u;//也不会报错
let u:void;
let num:number=u;// Type 'void' is not assignable to type 'number'.
任意值
- 任意值(Any)用来表示允许赋值为任意类型。
什么是任意值类型
- 如果是一个普通类型,在赋值过程中改变类型是不被允许的:
let renyiString:string='string';
renyiString=8;
//Type 'number' is not assignable to type 'string'.
- 但如果类型是Any,类型,则允许被赋值为任意类型:
let renyiString:any='string';
renyiString=8;
- 在任意值上访问任何属性都是允许的:
let anyThis:any='hello';
console.log(anyThis.myName)
console.log(anyThis.myName.firstName)
- 也允许调用任何方法:
let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');
- 可以认为,声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值。
未声明类型的变量
- 变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型:
let something;
something = 'seven';
something = 7;
something.setName('Tom');
- 等于
let something: any;
something = 'seven';
something = 7;
something.setName('Tom');
数组的类型
基础表示
- 「类型 + 方括号」表示法
let numbers:number[]=[1,2,3,4,5]
- 此时不允许出现其他类型,而且如果使用数组中push等方法,添加元素也得符合相应类型。
数组泛型
- 我们也可以使用数组泛型(Array Generic) Array<elemType> 来表示数组
let fibonacci:Array<number>=[1,2,3,4,]
- 如果数组中又有number类型又有string类型,则可以用|符号来区别定义:
let arr:(number|string)[]=['tom',1];
数组中对象类型的定义
- 项目中经常遇到数组中有对象的存在,对于这种就比较麻烦了,比如:
let arr:{name:string,age:number}[]=[
{name:'tom',age:18}
]
- 这样就比较麻烦了,我们可以使用ts中的类型别名来解决这个问题:
type PeopleType={name:String,age:Number};
let arr:PeopleType[]=[
{name:'bob',age:19}
]
- 也可以用类型定义也可以解决:
class PeopleType{
name:string;
age:number
}
let arr:PeopleType[]=[
{name:'bob',age:19}
]
元组的使用和类型约束
- 在数组中如果里面又有string和number,可以使用|来进行定义,但一定程度上并不严格。比如改成下面这种格式:
let arr:(number|string)[]=[111,'222',111];
- ts并没有报错,如果想要严格限制,则可以这样进行约束:
let arr:[number,string,number]=[111,'222',111];
Interface接口
- 比如我们要做一个筛选,吧不符合条件的过滤出去,我们可能会这样写:
const types=(name:string,age:number,height:number)=>{
age>=20 && height>=180 && console.log('符合条件');
age<=20 && height <180 &&console.log('不符合')
};
types('tom',20,180) //符合条件
- 但如果又修改了一些需求,可能还会去大量变更代码,在开发中,代码能复用肯定是最好的,所以可以把一些重复的代码抽离出来:
interface People{
name:string;
age:number;
height:number;
}
const types=(people:People)=>{
people.age>=20 && people.height>=180 && console.log('符合条件');
people.age<=20 && people.height <180 &&console.log('不符合')
};
const choose=(people:People)=>{
console.log(people.name+'----'+people.age+'---'+people.height)
}
const people={
name:'tom',
age:18,
height:178
}
types(people);
choose(people);
接口与类型别名的区别
- 看起来两者没有什么区别,但有个小细节:类型别名可以直接给类型,接口必须代表对象
type People=string;
interface People{
name:string;
age:number;
height:number;
}
- 如果传入的参数中有不确定项,我们可以使用 ?: 来进行处理:
interface People{
name:string;
age:number;
height:number;
say?:string
};
const types=(people:People)=>{
people.age>=20 && people.height>=180 && console.log('符合条件');
people.age<=20 && people.height <180 &&console.log('不符合')
};
const people={
name:'tom',
age:18,
height:178
}
const people2={
name:'tom',
age:18,
height:178,
say:'hello'
}
types(people);
types(people2);
- 这时候又有新需求了,如何在后面加入任意多的字段?这时候我们就可以这么写:
interface People{
name:string;
age:number;
height:number;
say?:string;
[propname:string]:any;
}
const types=(people:People)=>{
people.age>=20 && people.height>=180 && console.log('符合条件');
people.age<=20 && people.height <180 &&console.log('不符合')
};
const people={
name:'tom',
age:18,
height:178,
say:'hello',
add:'new add',
addNumber:123
}
types(people);
接口里的方法
- 接口不仅仅可以存属性,也可以存方法:
interface People{
name:string;
age:number;
height:number;
goto():string;
}
const types=(people:People)=>{
people.age>=20 && people.height>=180 && console.log('符合条件');
people.age<=20 && people.height <180 &&console.log('不符合')
};
const people={
name:'tom',
age:18,
height:178,
goto(){
return 'hello'
}
}
types(people)
接口和类的约束
- 在ES6中是有类的概念,类可以和接口相结合:
interface People{
name:string;
age:number;
height:number;
goto():string;
}
const types=(people:People)=>{
people.age>=20 && people.height>=180 && console.log('符合条件');
people.age<=20 && people.height <180 &&console.log('不符合')
};
const people={
name:'tom',
age:18,
height:178,
say:'hello',
add:'new add',
addNumber:123,
goto(){
return 'hello'
}
};
class newPeople implements People{
name='bob';
age=19;
height:190;
say:'hello';
add:'new add2';
addNumber:123;
goto(){
return 'hi'
}
};
let a=new newPeople();
console.log(a.goto())
types(people)
接口的继承
- 接口与接口也是可以继承的:
interface People{
name:string;
age:number;
height:number;
say?:string;
[propname:string]:any;
goto():string;
}
const types=(people:newPeople)=>{
people.age>=20 && people.height>=180 && console.log('符合条件');
people.age<=20 && people.height <180 &&console.log('不符合')
};
const people={
name:'tom',
age:18,
height:178,
say:'hello',
add:'new add',
addNumber:123,
goto(){
return 'hello'
},
back(){
console.log(1)
}
}
interface newPeople extends People{
back():void
}
types(people)
类的基本使用
- 首先,我们先创建一个类:
class Test{
name='say'
say(){
return this.name
}
};
- 这就是平时所写的类,在ts中的继承和ES6的继承是一样的,关键字也是extends,比如我们这里新建个类,继承Text;
class Test{
name='say'
say(){
return this.name
}
};
class NewTest extends Test{
back(){
return 'hello'
}
}
const tom=new NewTest();
console.log(tom.say());
console.log(tom.back());
super关键字的使用
- 如果想在say方法中后面加点东西,可以这么操作:
class Test{
name='say'
say(){
return this.name
}
};
class NewTest extends Test{
back(){
return 'hello'
}
say(){
return super.say()+'----hello'
}
}
const tom=new NewTest();
console.log(tom.say());
console.log(tom.back());
ts中类的访问类型
- ts中的访问类型就是基于三个关键字:private、protected以及pubilc这三种访问类型。看例子,先定义一个类,然后用这个类的对象,进行赋值:
class Person{
name:string
}
const person=new Person();
person.name='tom';
console.log(person.name)
pubilc
- 运行可以看到正常的输出内容,这是因为如果不对name的访问属性进行定义,那么他的默认属性就是pubilc,从字面意思来看,它的意思就是公用的,允许在类的内部和外部被调用。
class Person{
public name:string
}
const person=new Person();
person.name='tom';
console.log(person.name)
private
- private的属性意思就是只允许在类的内部调用,不能在外部调用
class Person{
private name:string;
public say(){
console.log(this.name+'-----hello');
}
}
const person=new Person();
person.name='tom';//报错
console.log(person.name)//报错
protected
- protected允许在类内以及继承的子类中使用,把刚刚的name改成protected属性,这时候在外部就会报错,这时候再写一个继承,代码如下:
class Person{
protected name:string;
public say(){
console.log(this.name+'-----hello');
}
}
const person=new Person();
person.name='tom';//报错
console.log(person.name)//报错
class NewPerson extends Person{
public back(){
console.log(this.name)
}
}
const newperson=new NewPerson();
newperson.back()//正常
类的构造函数
- 首先新建一个类:Person,定义一个name,并在new的时候进行参数传递,然后打印出来,这时候我们就可以使用构造函数constructor :
class Person{
pubilc name:string;
constructor(name){
this.name=name
}
};
const person=new Person('tom');
console.log(person.name);
- 可以看到可以打印出来,但是上面写法有点麻烦,还可以再进行简化:
class Person{
constructor(pubilc name:string){}
};
const person=new Person('tom');
console.log(person.name);
类继承中的构造器写法
- 普通的书写方法上面已经演示了,在子类中使用构造函数需要用**super()**调用父类的构造函数,直接看代码:
class Person{
constructor(pubilc name:string){}
};
class Teacher extends Person{
constructor(pubilc age:number){
super()
}
};
const teacher=new Teacher(18);
console.log(teacher.age+'---'+teacher.name);
ts中类的Getter、Setter、static以及readonly
- 在上面中提到了访问类型private,它的最大用处就是封装一个书写,然后通过Getter和Setter去访问和修改:
class Person{
constructor(private _age:number){
}
}
- 如果想让别人知道,就可以使用Getter来实现,他并不是一个方法,只是一个属性:
class Person{
constructor(private _age:number){
};
get age(){
return this._age
}
};
const person=new Person(30);
console.log(person.age)
- 这时候你可能会觉得这不是多此一举吗?但在Getter中可以对 _age进行处理:
class Person{
constructor(private _age:number){
};
get age(){
return this._age-10
}
};
const person=new Person(30);
console.log(person.age)
- 既然 _age是私有的,我们无法进行改变,这时候就可以用Setter进行改变:
class Person{
constructor(private _age:number){
};
get age(){
return this._age
}
set age(age:number){
this._age=age
}
};
const person=new Person(30);
person.age=20;
console.log(person.age)
- 在类中,如果想用这个类的实例,就必须先进行new操作,但有没有一种方法不需要new就可以?
//常规方法
class Person{
say(){
return 'say hello'
}
};
const person=new Person();
console.log(person.say());
- 在ts中,我们不想new的话可以这么写:
class Person{
static say(){
return 'say hello'
}
};
console.log(Person.say())
- readonly在初始化后赋值,以后就不能进行修改
class Person{
constructor(readonly name:string){}
}
let p=new Person('tom');
p.name='bob'//报错
console.log(p.name);
类的抽象类
-
abstract 用于定义抽象类和其中的抽象方法。它有几个特点:
-
抽象类是不允许实例化的
//错误演示
abstract class Person {
public abstract say()
}
const tom=new Person()//无法创建抽象类的实例
- 抽象类中的抽象方法必须被子类实现:
//错误演示
abstract class Person {
abstract say(){
console.log('hello')
}
}
class Tom extends Person{
say(){
console.log('say hello')
}
};
//正确方法
abstract class Person {
abstract say()
}
class Tom extends Person{
say(){
console.log('say hello')
}
};
tsconfig.json配置
生成
- 我们可以通过tsc --init去生成ts配置文件:
tsc --init //终端执行
编译
- 会生成一个tsconfig.json文件,我们在ts文件中可以随便写点什么:
//测试tsconfig.json
const test:string='tsconfig.json';
- 然后打开tsconfig.json文件,找到complilerOptions属性下的removeComments:true(这个配置是编译后不输出注释),取消掉注释,然后执行命令:
tsc
- 这时候打开生成的js文件,发现没有注释,说明成功。
include 、exclude 和 files
- 如果有多个ts文件,只想编译一个可以在ts配置项中加入include:
"include":["text.ts"],
- 如果想除了某个文件不编译,剩下的都编译,可以使用exclude:
"exclude":["text.ts"],
- files和include没有什么区别:
"files":["text.ts"],
compilerOptions配置
removeComments
- 这个配置意思就是编译后不输出注释
strict
- 这个设置为true,就代表严格执行ts语法,要严格按照ts语法来编写。
noImplicitAny
- 允许你的注解类型 any 不用特意表明,如果此时设置了true,看例子:
//这时候编译会报错
function test(name) {
return name;
}
//正确
function test(name:any) {
return name;
}
strictNullChecks
- 意思就是,不强制检查null类型,此时如果配置为true看例子:
//此时就不会报错
const test: string = null;
outDir和rootDir
- 此项配置是来指定文件目录和打包后存放目录,rootDir为文件目录,outDir为打包后保存的目录
{
"outDir": "./build" ,
"rootDir": "./src" ,
}
编译ES6语法
- 可以使用target和allowJs,target默认为true
"target":'es5' , // 这一项默认是开启的,你必须要保证它的开启,才能转换成功
"allowJs":true, // 这个配置项的意思是联通
sourceMap
- sourceMap 简单说,Source map 就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。有了它,出错的时候,除错工具将直接显示原始代码,而不是转换后的代码。这无疑给开发者带来了很大方便。
noUnusedLocals
- 设置noUnusedLocals为true,编译代码:
const name:tring='111';
export const age = "text";
- 这时候就会报错,因为有name变量没有使用。
更多
- 更多可以查看 配置查询网站
联合类型和类型保护
联合类型
- 联合类型的意思就是允许一个类型有两种或者两种以上的类型:
interface Test{
name:string;
say:()=>{
}
}
interface Test2{
name:string;
call:()=>{};
}
function Tom(fun: Test | Test2){
console.log(fun.name)
}
- 如果此时修改一下方法:
function Tom(fun: Test | Test2){
fun.say()
}
//报错
- 这是因为只能访问两个类型的共有方法。
类型保护-类型断言
- 上面的方法,如果修改完报错,这时候我们可以用as来判断:
interface Test {
text: boolean;
say(): void
}
interface Test2 {
text: boolean;
skill():string
}
function judgeWho(val: Waiter | Teacher) {
if (val.text) {
(val as Teacher).skill();
}else{
(val as Waiter).say();
}
}
const a={
text:true,
skill:function():void{
console.log(1)
},
say:function():void{
console.log(2)
}
}
judgeWho(a) //1
类型保护-in语法
- in方法与断言比较类似,使用方法如下:
interface Person{
name:string;
say:()=>{
}
}
interface Person2{
name:string;
todo:()=>{
}
};
function tom(val:Person|Person2){
if('todo' in val){
val.todo()
}else{
val.say()
}
}
类型保护-typeof语法
- 可以用typeof方法来判断:
function add(name:string|number,name2:string|number){
if(typeof name==='string'||typeof name2=='string'){
return name+'---'+name2
}else{
return name+name2
}
}
add(1,2)
类型保护-instanecof语法
- 如果要保护类型是一个对象,就可以使用instanceof:
class NumObject{
num:number
}
function numbers(num1:object|NumObject,num2:object|NumObject){
if(num1 instanceof NumObject && num2 instanceof NumObject){
return num1.num+num2.num
}
}
Enum枚举类型
- 我们平时会有这种写法:
function getName(status:any){
if(status===0){
return 'one'
}else if(status===1){
return 'two'
}else{
return 'three'
}
}
console.log(getName(0))
- 这么写可能有点麻烦,阅读起来还是有点麻烦,这时候可以这么写:
const Status={
ONE:0,
TWO:1,
THREE:3
}
function getName(status:any){
if(status===Status.ONE){
return 'one'
}else if(status===Status.TWO){
return 'two'
}else{
return 'three'
}
}
console.log(getName(0));
- 这时候我们的枚举就要上场了:
enum Status{
ONE,
TWO,
THREE
}
function getName(status:any){
if(status===Status.ONE){
return 'one'
}else if(status===Status.TWO){
return 'two'
}else{
return 'three'
}
}
console.log(getName(1));
- 一样也可以输出,因为枚举是有对应数字值的,默认从0开始,当然也可以改变:
enum Status{
ONE=1,
TWO,//2
THREE//3
}
- 也可以进行返查操作:
enum Status{
ONE,
TWO,
THREE
}
function getName(status:any){
if(status===Status.ONE){
return 'one'
}else if(status===Status.TWO){
return 'two'
}else{
return 'three'
}
}
console.log(Status.ONE,Status[1]);
泛型
- 泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
在函数中使用
- 我们先编写一个函数:
function add(one:string,two:string){
return `${one}${two}`
}
console.log(add('a','b'))
- 这时候我们希望参数更加灵活一些,两个参数为number或者string:
function add(one:string|number,two:string|number){
return `${one}${two}`
}
console.log(add('a','b'))
- 这么书写有些麻烦,这时候就可以使用泛型:
function add<T>(one:T,two:T){
return `${one}${two}`
}
console.log(add<string>('a','b'))
- 在使用中,泛型通常用 <T> 来进行表示。泛型可以有多个吗?当然可以:
function add<T,P>(one:T,two:P){
return `${one}${two}`
}
console.log(add<string,number>('a',2))
- 同时泛型也支持类型推断:
function add<T,P>(one:T,two:P){
return `${one}${two}`
}
console.log(add<string,number>('a',2))
在类中使用
- 首先先写一个类,且接受参数为一个数组,数组里面存放string类型的数据:
class List {
constructor(private list:string[]) {};
getItem(index:number):string{
return this.list[index]
}
};
const list=new List(['girl','boy','wom']);
console.log(list.getItem(1))
- 这时候如果我们传递数组时候里面又想放数字怎么办:
class List {
constructor(private list:string[]|number[]) {};
getItem(index:number):string|number{
return this.list[index]
}
};
const list=new List(['girl','boy','wom']);
console.log(list.getItem(1))
- 这么写就比较复杂了,这时候就可以使用泛型来简化我们的代码:
class List<T>{
constructor(private list:T[]) {};
getItem(index:number):T{
return this.list[index]
}
};
const list=new List(['girl','boy','wom']);
console.log(list.getItem(1))
- 发现上面的代码没有报错?因为类型推论,所以不会报错,严格意义上应该new的时候加上类型:
const list=new List<string>(['girl','boy','wom']);
- 还有一种场景,传递过来是一个数组对象,这时候可以通过继承来解决:
interface People{
name:string
}
class List<T extends People>{
constructor(private list:T[]) {};
getItem(index:number):string{
return this.list[index].name
}
};
const list=new List(
[
{name:'boy'},
{name:'girl'}
]
);
console.log(list.getItem(1))
泛型约束
- 上面例子中,泛型可以为任意值,但有时候我们希望还是能稍微约束一下:
function list<T extends number|string>(name:T){
return `${name}`
}
console.log(list<number>(1))
class List<T extends number|string> {
constructor(private list:T[]) {
};
getItem(index:number):T{
return this.list[index]
}
}
const list=new List([1,2])
console.log(list.getItem(0))
Namespace命名空间
新建一个ts项目
-
首先,我们建立一个项目文件,然后npm init -y生成package.json文件,然后再tsc -init生成ts配置文件。
-
在根目录下新建index.html文件,再建立一个src和bulid目录,在src目录下新建一个index.js。
-
配置tsconfig.json文件,设置入口和输出目录(outDir和rootDir)。
-
打开index.html,引入js文件:
<script src="./build/index.js"></script>
- 在新建的ts文件中随便写点什么:
console.log('hello');
- 然后tsc编译一下,打开控制台,我们就可以看到了
编写一个小组件
- 在我们刚刚建立的index.ts文件,写一个header、content和footer组件:
class Header {
constructor() {
const elem = document.createElement("div");
elem.innerText = "This is Header";
document.body.appendChild(elem);
}
}
class Content {
constructor() {
const elem = document.createElement("div");
elem.innerText = "This is Content";
document.body.appendChild(elem);
}
}
class Footer {
constructor() {
const elem = document.createElement("div");
elem.innerText = "This is Footer";
document.body.appendChild(elem);
}
}
class Page {
constructor() {
new Header();
new Content();
new Footer();
}
}
- 然后在index.html中加一行js代码:
<script>new Page();</script>
- 这时候我们可以看到内容正常输出,但有一个问题,我们的Header、content和footer都暴露了出来,并不是只暴露一个page,这时候我们的命名空间就派上了用场:
命名空间
- 命名空间声明的关键词是namespace 比如声明一个namespace Home,需要暴露出去的类,可以使用export关键词,这样只有暴漏出去的类是全局的,其他的不会再生成全局污染了。
namespace Home {
class Header {
constructor() {
const elem = document.createElement("div");
elem.innerText = "This is Header";
document.body.appendChild(elem);
}
}
class Content {
constructor() {
const elem = document.createElement("div");
elem.innerText = "This is Content";
document.body.appendChild(elem);
}
}
class Footer {
constructor() {
const elem = document.createElement("div");
elem.innerText = "This is Footer";
document.body.appendChild(elem);
}
}
export class Page {
constructor() {
new Header();
new Content();
new Footer();
}
}
}
- 这么写也是比较麻烦,因为我们需要引入两个文件,我们可以通过配置来让他成为一个文件,打开tsconfig.json,找到这一行:
"module":"commonjs"
//修改为:
"module":"amd
- 然后找到这一行:
{
"outFile": "./build/index.js"
}
子命名空间
- 如果在刚刚的组件中再写一个会怎么样?
namespace Components {
export namespace SubComponents {
export class Test {}
}
//someting ...
}
//读取
// Components.SubComponents.Test
ts中使用import
- 首先建立一个文件compontent.ts,随便写点东西,然后使用export导出:
export class Header {
constructor() {
const elem = document.createElement("div");
elem.innerText = "This is Header";
document.body.appendChild(elem);
}
}
export class Content {
constructor() {
const elem = document.createElement("div");
elem.innerText = "This is Content";
document.body.appendChild(elem);
}
}
export class Footer {
constructor() {
const elem = document.createElement("div");
elem.innerText = "This is Footer";
document.body.appendChild(elem);
}
}
- 然后在index.ts中导入一下:
import { Header, Content, Footer } from "./compontent";
export class Page{
constructor(){
new Header();
new Content();
new Footer();
}
}
- 运行tsc编译,打开build中的index.js,可以看到代码是define开头的,这是 amd 规范的代码,不能直接在浏览器中运行,可以在 Node 中直接运行,所以我们还需要借助require.js的支持:
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.js"></script>
- 然后在index.html中使用require.js写法:
<script>
require(["page"], function (page) {
new page.default();
});
</script>