吐血整理TypeScript语法
认识TypeScript
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. 翻译一下:TypeScript是拥有类型的JavaScript超集,它可以编译成普通、干净、完整的JavaScript代码。 可以理解成TypeScript就是JavaScript加强版,是在JavaScript的基础上添加类型约束,而且包含了一些语法的扩展比如新增了一些新的类型比如:枚举类型、元组类型。在编译时期会将TypeScript代码转换为JavaScript代码。也就是说最终运行在浏览器上的还是JavaScript(本质上还是没有修改)
TypeScript的编译以及运行环境
编译环境
上文说到了TypeScript是需要在编译阶段转换成JavaScript然后才能运行到浏览器的,所以这里需要安装TypeScript,利用TypeScript的Compiler将其编译成JavaScript, 编译过程如下:
安装命令: 全局安装:npm install typescript -g(也可以安装到自己的项目中) 查看版本: tsc --version(如果是安装到自己的项目中的话指令前面需要添加npx) 编译测试:
运行环境
webpack(项目开发一般使用这种方式)
具体配置方式可以参考官网。大概原理都一样还是使用TypeScript的编译器将ts代码转换成js代码运行在浏览器中(这里需要注意的是typescript必须要在本地依赖一下应为webpack会在本地去查找TypeScript的依赖)
ts-node库
通过ts-node库,为TypeScript的运行提供执行环境;一般学习语法的时候可以使用这种方式。 使用方式: 安装ts-node: npm install ts-node -g 另外ts-node需要依赖 tslib 和 @types/node 两个包 npm install tslib @types/node -g 现在就可以直接通过ts-node运行TypeScript代码了: ts-node index.ts 执行结果:
TypeScript语法
变量的声明
在TypeScript中定义变量需要指定 标识符 的类型,声明了类型后TypeScript就会进行类型检测,声明的类型可以称之为类型注解。具体格式如下: var/let/const 标识符: 数据类型 = 赋值; 具体例子:如果我们需要声明一个name字段: let name:string = "土豆" 如果此时将name复制给一个number类型就会报错: 这里有几个注意点:
- 其实上述的赋值不写类型注解也行,应为name会赋值给一个确切的字符串的值,通过类型推导(后文会说到),ts就能确定name的类型是string
- 类型注解string,也可以写成String,包括number写成Number,boolean写成Boolean等,建议写小驼峰类型,String、Number等其实是一个对象,是string和number的包装类,里面存储的不仅仅是对应的值还有一些其他的属性、方法:
声明变量的关键字
这里就和js一样了都是let、var、const。 但是不推荐使用var,应为es6之后let、var区别就是一样的了,但是var是没有块级作用域的如下: 可以发现代码块以外name是不能访问了因为name只在上面的代码块中有效,代码块之外就被释放了,所以访问不到,但是name2还是能呗访问的,也就是说代码块执行完了name2也没有被释放,这里也造成了内存浪费
数据类型
JavaScript数据类型
number
TypeScript和JavaScript一样,不区分整数类型(int)和浮点型 (double),统一为number类型:
let num: number = 123 //十进制
let num1: number = 0b110 //二进制
let num2: number = 0o233 //八进制
let num3: number = 0x1f //十六进制
boolean
boolean类型只有两个取值:true和false
let flag: boolean = false
flag = 20 > 30 //也可以复制一个判断表达式
string
let str: string = "tudou"
str = `tudou`
Array
定义方式有两种 第一种声明方式: 第二种声明方式: 不建议使用第二种方式,应为<>会和jsx语法冲突,能使用第一种声明方式就是用第一种方式 解决方案: 加个逗号
object
TypeScript 2.2 引入了被称为 object
类型的新类型,它用于表示非原始类型。在 JavaScript 中以下类型被视为原始类型:string
、boolean
、number
、bigint
、symbol
、null
和 undefined
。
const person: object = {
name: '土豆',
age:17
}
let test:object = new String('test111')
赋值原始类型会报错:
Symbol
在es5指向我们想让一个对象中存在同样名称的两个key是没有办法做到的应为对象的key是唯一的,但是后面推出了Symbol类型可以帮我帮我们实现一个对象中存在同样名称的两个key,如下:
个人感觉没啥用,底层原理key还是唯一的Symbol仅仅只是做到了值不一样,其实底层的内存地址还是不一样的
null和undefined类型
TypeScript数据类型
any
某些情况下再声明的时候无法确定变量的类型此时就可以使用any. 可以将任何值传给any类型的变量 所以不建议使用any类型
unknown
上图将any
类型换成unknown
,乍一眼去看好像和any很像,也能将各种类型赋值给unknow类型但其实不然,仔细看的话下面在调用num.length
的时候报错了。原因是unknown
并没有放弃类型检测,区别于any
的是any
直接放弃了类型检测,所以不建议使用any
。
void
void
表示不返回值的函数的返回值。只要函数没有任何 return
语句,或者没有从这些 return
语句返回任何显式值,它就是推断类型:需要注意的是如果没有任何返回值js
会帮你隐式的返回 undefined
,void
同样的也可以返回undefind
//如果不写返回值默认返回void(这里是类型推导返回void)
function sum(num1: number, num2: number) {
console.log(num1+num2);
}
//上面的写法等价与这种写法
function mult(num1: number, num2: number):void {
console.log(num1 * num2);
}
function sum(num1: number, num2: number):void {
return undefined
}
never
never
表示永远不会发生值的类型。
如果一个函数中是一个死循环或者抛出一个异常,此时这个函数就不会有任何返回值就可以返回 never
(void会帮你隐式返回 undefined
)如下代码:
function testFunc():never {
while (true) { }
}
function testFunc1():never {
throw new Error()
}
这样一看感觉没啥用,应为我们实际开发中基本上也不会这样写,再看一下下面的例子
function testFunc2(arg: number | string | boolean) {
switch (typeof arg) {
case 'number':
console.log(arg)
break;
case 'string':
console.log(arg.length)
break;
case 'boolean':
console.log(arg ? '是' : '否')
break;
default:
let neverArg: never = arg
break;
}
}
testFunc2(111)
testFunc2(false)
testFunc2("123")
//如果A同学封装了一个共用函数,但是这个函数只对number | string | boolean //这三个类型做处理,其他类型就赋值给never类型的参数 //这样的话如果B同学用这个函数他想传递一个数组给这个函数 //一般不知道业务逻辑的人就直接在函数的入参再添加数组这个类型 如下: 这样的话函数调用就不会报错,如果没有下面一句 函数的执行也不会报错,这样就出现了bug,如果加上上面一句的话函数执行就会报错 这个B同学就会知道这个函数还需要添加一些针对数组的逻辑,这样就避免了一些不必要的bug
tuple(元组类型)
元祖就是一个明确元素数量以及每个元素类型的一个数组。
一般我们声明数组的话不管数组是 any
类型的还是说是一个联合类型的,都不能确定数组中的元素是什么类型。元组不同。如下:
应为元组在定义的时候就已经确定了数组的容量以及数组每个元素的类型,如果少一个元素会报错,元素类型对不上也会报错所以用的时候也就不需要那么多判断。
实际应用如下:
函数
函数的参数类型
声明函数时,可以在每个参数后添加类型注解,以声明函数接受的参数类型。如下:
函数的返回值类型
可以再函数列表的后面添加返回值类型如下:
一般情况下是可以不写返回值类型的,应为TypeScript会根据 return
返回值推断函数的
返回类型
匿名函数的参数类型
匿名函数和函数声明的区别是当一个函数出现在TypeScript可以确定该函数会被如何调用的地方时该函数的参数会自动指定类型。如下 当然写上类型注解也没错: 需要注意的是: 所以如果是元组类型,这里就需要添加类型判断了
默认参数
给入参赋值就行如下:(下面arg参数就变成了 number
和undefined
的联合类型)
剩余参数
从ES6开始,JavaScript也支持剩余参数,剩余参数语法允许我们将一个不定数量的参数放到一个数组中。(需要注意的是剩余参数比如是参数列表中的最后一个参数)
function print(arg:number,...args:number[]){
console.log(args);
}
函数类型
我们可以编写函数类型的表达式,来表示函数类型;如下:
(x:number,y:number) => number
表示这个函数接受两个参数并且都是 number
类型的参数,并且这个函数的返回值也是一个 number
类型的一个值,具体用法如下:
function handle(callback:(x:number) => number){
console.log(callback(123));
}
handle((x:number) => {
return x;
})
函数重载
如果有这样一个场景,编写一个函数,需要传入两个参数,如果参数的类型是 number
做相加操作,如果两个参数的类型是 string
则做字符串拼接的操作。如果不使用函数重载只能通过如下方式实现:
function add(arg1: number | string, arg2: number | string) {
if (typeof arg1 === "number" && typeof arg2 === "number") {
console.log(arg1 + arg2);
} else if (typeof arg1 === "string" && typeof arg2 === "string") {
console.log(`参数1:${arg1} 参数2:${arg2}`);
}
}
这样做的问题就在于我们不能限制调用者传递两个不同类型的参数,还一个就是代码判断需要写很多。 在TypeScript中,我们可以去编写不同的重载签名来表示函数可以以不同的方式进行调用; 函数重载的实现方式: 这里引用一下官方的例子:
function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
if (d !== undefined && y !== undefined) {
return new Date(y, mOrTimestamp, d);
} else {
return new Date(mOrTimestamp);
}
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3);
需要注意的是一定要写一个不是很具体类型的函数: 而且这个函数一定要在最后面 原因是ts是冲上往下匹配的如果第一个就匹配上了下面的就不会再匹配了
对象类型
如下所示函数需要接受一个对象参数,并且确定对象中的值
function printCoordinate(point:{x:number,y:number}){
console.log("x坐标" + point.x);
console.log("y坐标" + point.y);
}
可选类型
对象类型中也可以指定某个属性是可选的如下:(属性后面添加问号就行)
可选类型可以看做是类型和 undefined
的联合类型:
联合类型
TypeScript的类型系统允许我们使用多种运算符,从现有类型中构建新类型。联合类型是由两个或者多个其他类型组成的类型,表示可以是这些类型中的任何一个值,联合类型中的每一个类型被称之为联合成员。 写法就是多种类型用竖线隔开如下:
let test:number|string;
test = 123
test = "123"
类型别名
如果代码中一个变量的类型时很多类型组合的联合类型或者是对象类型,然后又需要在很多地方使用这个时候可以考虑使用类型别名,用法如下: 建议一些函数的声明使用类型别名这样代码看起来更简洁如下: 如果不是用别名的话代码就是这个样子的:
类型断言as
在有些特殊的情况下 TS 无法去推断出来一个变量的具体类型,而作为开发者根据代码的使用情况是可以明确知道这个变量到底是什么类型的.
例如:我们需要通过 document.getElementById
方法获取一个 img
标签,TypeScript只知道该函数会返回 HTMLElement
,但并不知道它
具体的类型此时就可以用类型断言:
const element = document.getElementById("img") as HTMLImageElement
element.src = ""
没有类型断言的情况下只能推导出是 HTMLElement
所以用 src
属性的时候报错
let test: unknown[] = ["qwe",123];
// test.length;
const test1 = test[0] as string
console.log(test1);
(test[0] as string).length
也可以以将一个确定的类型断言为一个不确定的类型,如下:
非空类型断言
用法很简单就是在参数后面添加感叹号。
如下代码如果不做任何处理就有可能报错:
应为 str
有可能是 undefined
如果添加上非空类型断言就不会报错如下:
非空类型断言的意思就是搞得编译器 str
这个值一定不是空。但是问题就来了,发现在下面还是能调用 print
方法并且传入一个空值,所以此时报错就不会出现编译阶段了而是运行阶段如下:
所以使用非空类型断言要谨慎,个人感觉大部分时候可以使用可选链替代非空类型断言
可选链
可选链实在 ES11中添加的新特性。使用方法就是可选链操作符 ?.
作用是当对象的属性不存在时,会短路,直接返回undefined,如果存在,那么才会继续执行;
还是上文代码中的例子将非空类型断言改成可选链如下:
??和!!的作用
- !!的作用:将一个其他类型转换成boolean类型
- ??的作用 空值合并操作符(??)是一个逻辑操作符,当操作符的左侧是 null 或者 undefined 时,返回其右侧操作数, 否则返回左侧操作数
字面量类型
在 TypeScript 中,字面量不仅可以表示值,还可以表示类型,即所谓的字面量类型。TypeScript 支持 3 种字面量类型:字符串字面量类型、数字字面量类型、布尔字面量类型。对应的字符串字面量、数字字面量、布尔字面量分别拥有与其值一样的字面量类型:
let str:"Hello World!" = "Hello World!"
let num: 999 = 999
let flag: true = true
字面量类型的值要和类型一致
一般开发中主要和联合类型一起使用比如封装请求方法如下:
function request(url: string, method: "GET" | "POST") {
console.log(url + method);
}
request("http://xxx.com","GET")
这个方法就只能传入两个值
但是也有一种特殊的情况如下:
这个就可以使用类型断言将 method
转成更确切的类型如下:
类型缩小
通俗点讲就是讲一个很宽泛的类型缩小成一个更具体的类型。也被称为类型保护。
比如讲一个联合类型转为其中一个类型。
将一个 unknown
类型转为 string
类型。
具体方式有以下几种:
- typeof
返回变量的类型如下:
{ function printLength(arg: string | number) { if (typeof arg == 'string') { console.log(arg.length); } else { console.log(arg); } } }
- instanceof
是个运算符来检查是否是某个对象的实例:
function printLength(arg: String | Number) { if (arg instanceof String) { console.log(arg.length); } else { console.log(arg); } }
instanceof
主要用在对象的判断上,应为只有对象才能实例化class Person { num = 100 } class Student { } function printLength(arg: Person | Student) { if (arg instanceof Person) { console.log(arg.num); } else { console.log(arg); } }
- in 是个运算符来确定对象是否具有带名称的属性:in运算符 如果指定的属性在指定的对象或其原型链中,则in 运算符返回true;
this的类型
- 可推导的this类型
上面的代码 ts能确定的推导出let person = { name: '土豆', printName() { console.log(this.name); } } person.printName()
this
的是指向person
的所以不会报错 - 不确定的this类型
换一种写法就会报错如下:
应为此时的
this
就不一定是指向person
有可能被其他对象调用 - 指定this的类型 这样ts 就不会报错
类的使用
类的定义
class关键字来定义一个类,再类的内部也可以添加属性,如果属性没有声明类型默认是any(如果属性不是可选的或者是添加了非空断言就必须要给属性赋值) 类可以有自己的构造函数,在通过new关键词创建一个实例的时候会调用构造函数 类中也可以有自己的函数,称之为类的方法如下:
类的继承
通过 extends
关键字实现类的继承:
class Person {
name?: string;
constructor(name: string) {
this.name = name
}
running() {
console.log("跑");
}
}
class Student extends Person {
age: number = 18
printInfo() {
console.log("姓名:" + this.name);
console.log("年龄:" + this.age);
}
}
let stu = new Student("土豆")
stu.printInfo()
子类中可以有自己的属性和方法,同时也可以集成父类的属性和方法
在子类中可以通过 super
关键字来调用父类的方法如下:
类成员的修饰符
- public 修饰的是在任何地方可见、公有的属性或方法,默认编写的属性就是public的
- private 修饰的是仅在同一类中可见、私有的属性或方法;
- protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法
只读属性readonly
如果有一个属性我们不希望外界可以任意的修改,只希望确定值后直接使用,那么可以使用readonly
:
getters/setters
一些私有属性我们是不能直接访问的,或者某些属性我们想要监听它的获取(getter
)和设置(setter
)的过程,
这个时候我们可以使用存取器。
静态成员
使用static
修饰的需要通过类去访问而不是通过实例访问
class Student {
static age: number = 18
static printInfo() {
}
}
Student.age = 20
Student.printInfo()
抽象类abstract
被 abstract
关键字修饰的类就是抽象类。
同样的被 abstract
关键字修饰的方法就是抽象方法。
但是需要注意的是
- 抽象类不能实例化
- 如果有子类继承一个抽象类,抽象类中的方法子类必修实现(除非子类也是抽象类)
- 抽象类中的抽象方法是不能实现的
abstract class Animal {
abstract numberOfLegs(): number;
}
abstract class Person extends Animal{
}
class Student extends Person {
numberOfLegs(): number {
return 2
}
}
class Dog extends Animal {
numberOfLegs(): number {
return 4
}
}
let stu = new Student()
let dog = new Dog()
function printLegs(animal: Animal) {
console.log(animal.numberOfLegs);
}
printLegs(dog);
printLegs(stu);
类的类型
类本身也是可以作为一种数据类型的:
接口
接口的声明
使用 interface
关键字,接口中的 可选类型 和 只读类型和上文的用法一致
interface Person {
name: string,
age: number,
running:() => void
}
class Student implements Person {
name = "我23";
age = 18;
running() {
console.log("跑");
};
}
//如果是被类实现了那么在传入类对象的时候就可以直接使用接口接受参数
function test(p:Person){
p.running()
}
let p = new Student();
test(p)
let teacher: Person = {
name:"我23",
age:123,
running() {
console.log("跑");
}
}
索引类型
前面我们使用interface
来定义对象类型,这个时候其中的属性名、类型、方法都是确定的,但是有时候我们会遇到类似下面的对象,
let scoreList = {
"zhangsan": 66,
"lisi": 88,
"土豆":99
}
对象的key和value的类型是确定具体有哪些key都是不确定的
这个时候可以使用索引类型,使用之后可以向对象中添加新值但是必须按照key是stirng
,value是number
类型的规则
接口定义函数
interface Func {
(arg:number,arg1:number):number
}
const add: Func = (arg: number, arg1: number) => {
return arg+arg1
}
建议仅仅是定义函数可以使用别名看起来更清晰
type Func = (arg:number,arg1:number) => number
接口的继承
接口和类一样是可以进行继承的,也是使用extends
关键字,接口继承区别于类继承的是接口继承可以实现多继承
交叉类型
上文说到了联合类型是使用 | 分割满足其中一种类型即可。 交叉类型则是使用 & 分割要同时满足多个类型才行。
type Test = number & string
上述 Test
类型其实就是一个never
类型,实际开发中主要是对对象类型的交叉如下:
interface Person1 {
age:number
}
interface Person2 {
name:string
}
let person: Person1 & Person2 = {
age: 18,
name:"土豆"
}
interface和type区别
两者其实没有太大的差别,如果是定义对象类型interface
可以重复的对某个接口来定义属性和方法;而type
定义的是别名,别名是不能重复的
个人建议:定义非对象类型,通常推荐使用type
,比如联合类型、一些Function
,其他建议使用interface
字面量赋值(类型擦除)
ts是这样实现的她会将对象类型中不存在的属性擦除掉然后在对比现在的属性是否全部满足对象类型如果满足就能赋值如果不满足则报错
枚举类型
枚举其实就是将一组可能出现的值,一个个列举出来,定义在一个类型中,这个类型就是枚举类型;枚举允许开发者定义一组命名常量,常量可以是数字、字符串类型,如下: 枚举中如果不定义枚举类型的值则是默认冲0开始的数值比如上文中的枚举其实就是下面这个样子的
enum Method {
GET = 0,
POST = 1
}
也可以只能冲100开始如下: 也可以赋值字符串:
泛型
软件工程的主要目的是构建不仅仅明确和一致的API,还要让你的代码具有很强的可重用性 泛型通俗点讲就是将参数的类型参数化,就是参数的类型也由调用者指定。 比如下面这个例子: 同学B封装了一个函数是打印字符串的长度,过了一个月发现这个方法不够用还要接受一个数组的参数并且打印数组的长度此时的做法一般是这样的:
//第一版
function printLength(arg: string) {
console.log(arg.length);
}
//第二版
function printLength(arg: string | number[]) {
console.log(arg.length);
}
//第三版
function printLength(arg: string | number[] | string[]) {
console.log(arg.length);
}
//第n版....
发现每个版本都在修改公共的方法,可能有些人想着可以使用 any
,但是 any
是放弃了类型检测的所以不建议使用。
泛型的基本使用(泛型方法)
针对上面的问题我们可以使用泛型这样子修改:
function printLength<T>(arg: T) {
if (typeof arg == 'string') {
console.log(arg.length);
}
}
//TS 类型推断,自动推导类型为 string
printLength("123")
//自己确定类型
printLength<string>("123")
printLength(["123",'123'])
这样修改至少每次我们的入参不需要再修改对应的类型了。 需要注意的是泛型方法调用方式有两种:
- 通过 <类型> 的方式将类型传递给函数;
- 通过类型推到,自动推到出我们传入变量的类型:
上文中的 T
是可以随意写的 可以写成 O
或者 Tudou
但是比较常用的名称是:
- T:Type的缩写,类型
- K、V:key和value的缩写,键值对
- E:Element的缩写,元素
- O:Object的缩写,对象
泛型接口
interface Person<T> {
name:T
}
let p: Person<string> = {
name:"123"
}
let p1: Person<number> = {
name:123
}
泛型类
class Person<T> {
name: T;
constructor(name: T) {
this.name = name
}
}
let p1 = new Person('123')
let p = new Person<string>('123')
泛型约束
有时候我们希望传入的类型有某些共性,但是这些共性可能不是在同一种类型中。
比如string
和array
都是有length
的,或者某些对象也是会有length
属性的
例如上文中的例子:我们是将参数类型参数化,但是里面实现代码应为有类型检测还是要对类型进行一个缩小,还是很麻烦的,这里我们就可以使用泛型约束再优化一下上述的代码
interface Common {
length:number
}
//泛型约束通过extends关键字类实现
//下文中的表示T类型之后至少要含有length属性如果没有则报错
function printLength<T extends Common>(arg: T) {
console.log(arg.length);
}
printLength("123")
printLength<string>("123")
printLength(["123",'123'])
这样就能完美的解决这个公共方法的问题了
类型的查找
上文说到的所有类型基本上都是我们自己自定义的类型都有明确的声明。但是也有一些其他类型比如:document.getElementById("img") as HTMLImageElement
。HTMLImageElement
哪里来的,我们都没有声明这种类型为什么能使用getElementById
这个方法为什么不报错,这和我们上文说到的有点相悖,上文说到只有声明了属性的类型之后才能调用。
这里就涉及到了typescript对类型的管理和查找规则了。 这里先说一下 .d.ts文件,上文说到的 .ts 是我们编写代码的地方最终都会被编译成 js文件但是 .d.ts文件仅仅是做类型声明(declare)的地方,只是在类型检测的时候会用到,不会将其编译成 js文件
typescript会以一下几种方式查找类型声明
-
内置类型声明 内置类型声明是typescript自带的、帮助我们内置了JavaScript运行时的一些标准化API的声明文件,包括比如Math、Date等内置类型,也包括DOM API,比如Window、Document(所以上文中的document操作不会报错)等(安装typescript就会带有)
-
外部定义类型声明 外部类型声明通常是我们使用一些库(比如第三方库)时,需要的一些类型声明。 有的第三方库会在自己的库中进行类型声明(编写.d.ts文件)。还有一部分第三方库通过社区的一个公有库DefinitelyTyped存放类型声明文件。可以通过这个网址查找要用的声明安装方式 www.typescriptlang.org/dt/search?s…
比如安装react的类型声明:npm i @types/react --save-dev 还比如
lodash
就是一个纯js库
,所以要添加外部类型声明 -
自己定义类型声明 可以直接创建 .d.ts文件 不需要引用只要创建就行,ts编译的时候会自动扫描。
转载自:https://juejin.cn/post/7107521574286147592