深入理解TypeScript基础语法

635 阅读11分钟

前言

大家好,我是大斌,前端菜鸟一枚,这段时间在学习TS,在学习的过程中也发现了一些比较神奇的地方,在思考和总结之后,我觉得将它们记录下来,供以后复习,同时分享给大家,希望能对你有用,哈哈!

1、静态类型

深度理解静态类型,如果我们给一个变量指定类型,其实不仅仅是指定他的基本类型,同时还指定了他的方法和属性;如const count:number = 1,这个语句不仅仅表示他的类型一定是number,同时他还具备了number这个类型所有的属性和方法

image.png

2、类型注解和类型推断

类型注解指的是我们告诉ts这个变量是什么类型,如:let count: number

类型推断是指TS会自动的去尝试分析变量的类型, let countInterface = 123;需要注意的是,就是这种类型推断只有我们将声明和赋值写在同一行的时候才有用,否则的话是失效的,举个例子:

let count ;
count = 1

这个时候你把 鼠标悬停到count上面试时你会发现他的类型是any而不是number了,所以这种情况我们只能给count加上类型注解才行;

let count: number;
count = 1

所以如果ts能够自动分析出变量类型的话,我们就什么也不用做了,否则我们就需要使用类型注解; 比如我们声明变量const firstNumber = 1,和变量cost secondNumber = 2,这两个变量我们就不需要加类型注解,把鼠标悬停在firstNumber和secondNumber上面,你会发现ts已经自动识别了他的变量就是number,而且永远都是1或者永远都是2;

那我们在声明一个变量 const total = firstNumber + secondNumber; 那这个变量需不需要加上类型注解呢?同样也是不需要的,因为TS也已经自动推断出了他的类型就是number;

我们再看一个例子,声明一个函数,给他传两个变量,然后返回他们的和;

function getTotal(firstNumber, secondNumber) {
  return firstNumber + secondNumber
}

const total = getTotal(1,2)

将鼠标悬停在total之上,你会发现他是any类型,这是因为函数的两个参数他都是any类型,TS无法自动推断出他的类型来,所以他的返回值也是any类型的,所以这个时候我们就需要给函数的两个参数加上number类型;

function getTotal(firstNumber: number, secondNumber: number) {
  return firstNumber + secondNumber
}

const total = getTotal(1,2)

这个时候你再看total他已经是number类型了,为什么我们只给函数参数添加类型之后,函数的返回值也是number类型呢?这也是类型推断的结果,鼠标悬停在getToal上,你会发现返回值类型也被推断数来是number了,所以就可以不在括号后面加上:number了;那是不是意味着我们就可以不写这个返回值类型了呢?那肯定不是的;举个例子;

function getTotal(firstNumber: number, secondNumber: number) {
  return firstNumber + secondNumber + ''
}

const total = getTotal(1,2)

上面的代码我们一不下心在后面多加了个空字符串,这个时候代码是不会报错的,但是函数的返回值是string类型的,但是,很明显我们需要的是一个数字,所以代码逻辑就有问题了;为了避免这个问题的返回,我们定义函数的时候,还是要将返回值类型写上的这样它就会在你编写代码的时候就给你报错,修改下上面的代码就正确了:

function getTotal(firstNumber: number, secondNumber: number):number  {
  return firstNumber + secondNumber
}

const total = getTotal(1,2)

再看对象,我们声明一个对象如下;

const obj = {
  name:'DaBin',
  age: 18
}

鼠标悬停在obj上,他会自动推断数name是string类型,age是number类型,这也是typescript的初衷,它希望我们给每个变量都能定义一个类型,如果没有的话,他会给我们推断出一个类型来;

3、深入理解函数

和JS一样,TS的函数声明也是有三种,关键字声明,表达式声明和箭头函数;

function func1() {}
const func2 = function() {}
const func3 = () => {}

在TS中,我们希望一个函数的参数和返回值都有一个类型声明,如何给一个函数加上类型声明呢?我们有两种写法,下面通过一个将字符串转换为数字的箭头函数类进行讲解:

const func = function(str: string): number {
  return parseInt(str, 10)
}

这种方法直接在函数体中对参数类型和返回值类型进行声明,这个时候将鼠标悬停在func上,就能看到函数的类型声明了,引入一个string类型的参数,返回一个number类的值,这是一种方法,然后介绍第二种;

const func1: (str: string) => number = (str) => {
  return parseInt(str, 10)
}

这种写法是将类型声明和函数体开发写的,(str: string) => number就是类型声明,然后你将鼠标悬停在func1上,就能看到和上面例子一样的类型声明了;两种方法都可以,具体使用哪种就看个人爱好了;但是,我们回头看第一个函数,思考下他的返回值类型真的有必要声明吗?其实是没必要的,因为类型推断能根据你的函数推断出了你的返回值就是一个number类型了,我们试着删除返回值类型注解;

image.png

看,函数类型注解还是一样的,所以在以后的工作中,函数返回值的类型,我们可以根据函数来判断有没有必要写;但是第二种写法,他是不能删除的,不然就会报错了;

4、数组和元组

TS的数组和JS里面的数组基本上是一样的,给数组加类型注解也是很简单的,只需在变量后面加上类型注解就行,如果不加的话TS也能做一下类型推断;

const numberArr = [1,2,3]
const numberArr2: number[] = [1,2,3]

对于简单的单类型的数组,即使我们不加类型注解TS也能帮我们推断出类型,我们也可以手动添加类型注解,如第二个例子;这是简单的数组,那如果一个数组有多种类型的话TS就不能自动推断出类型了,只能我们手动添加添加也很简单,就跟下面一样;

const Arr:(number | string )[] = [1,2,'hello']

除此之外,还有一些特殊的数组,如undefined数组,他的元素只能是undefined,否则的话就会报错;

const undefinedArr: undefined[] = [undefined]

除了基础类型之外,数组肯定还能存储对象类型的值,那对象类型的数组注解应该怎么写呢?别急,请往下看;

const objectArr: {name: string, age: number}[] = [{
  name: 'DaBin',
  age: 18
}]

上面的例子,我们声明了一个存储对象类型的数组,它的每一个元素都必须包含string类型的name和number类型的age两个属性,而且只能包含这两个属性,多了或者少了都会报错,这就是一个对象类型的数组的类型注解的写法,但是,这样写太难看了,如果对象的元素多的话,就会显得很冗长也不容易阅读理解,所以我们需要借助类型声明类优化一下;

type User = {
  name: string,
  age: number 
}

const objectArr: User[] = [{
  name: 'DaBin',
  age: 18,
}]

这样是不是就好多了呢?

元组 tuple

相信在学习TS之前,很多人都跟我一样,没听过元组这东西,其实元组也很好理解,他和数组一样,但是他的长度和类型是定死了的,就是说如果你知道一个数组,他的每一个值的类型以及这个数组的长度,那你就可以把它当做元组;比如下面这个数组;他的长度只能为3,而且他的第一个值必须是string类型,第二个是number类型,第三个是boolean类型,而且他们的顺序不能改变,那它就是一个元组;

const tupleArr: [string, number, boolean] = ['Dabin', 18, true]

上面这个数组,当你试图去改变任一元素位置的时候都会报错;这就是元组了;

5、接口

接口的声明方式有两种,type和interface,他们的的区别不大,唯一的区别就是type可以表示单个类型,如type=string ,而interface就不行了,他只能一个通用的对象或者方法,所以我们使用的原则就是能用interface就用interface;

在匹配接口的过程在,传入的对象必须含有接口里面的所有参数及类型匹配,如果传入的对象的参数比接口定义的多也是可以的,看下面这个例子,我们定义了一个函数,他的参数是Person接口类型的,然后打印name属性;我们调用这个函数,注意,这里传入函数的person对象中与Person接口相比,多了一个height属性;但是能正常执行;

function getPerson(person: Person): void {
  console.log(person.name);
  
}

const person = {
  name: 'Dabin',
  height: 180
}

getPerson(person)

上面的代码是不会报错的,也能正常打印,但是,如果我们不通过person这个中间变量,而是直接通过对象字面量的形式传入参数,就会报错了,这是因为,通过对象字面量传入的参数TS会进行强校验,所以会报错;这个在以后的工作中一定要特别注意;

function getPerson(person: Person): void {
  console.log(person.name);
}

getPerson({
  name: 'Dabin',
  height: 180 //报错!类型“{ name: string; height: number; }”的参数不能赋给类型“Person”的参数。
})

这个时候我们就需要一个中间缓存变量来存储这个对象,然后再传入函数了,就像上面的例子那样;如果不想这么做那怎么办呢?那我们就只能更改接口了,让他可以还有任何其他属性,属性名必须是string类型,值可以是any类型;

interface Person {
  name: string,
  age?: number,
  [propName: string]: any
}

这样,函数也不会报错了,这个接口的意思就是它必须有一个name属性和一个可选的age属性,也可以有其他的属性,这些属性也是可选的;

接口也可以定义方法,如,我们给上线的接口增加一个say方法;

interface Person {
  name: string,
  age?: number,
  [propName: string]: any,
  say(): string
}

function getPerson(person: Person): void {
  console.log(person.name);
}

const person = {
  name: 'Dabin',
  height: 180
}

getPerson(person)

再看这个例子,我们给接口增加可方法之后函数就报错了,说person缺少say方法,所以我们需要给person对象加上这个方法;

...
const person = {
  name: 'Dabin',
  height: 180,
  say(){
    return 'hello'
  }
}

接口也可以继承,比如我们定义一个teacher接口继承Person接口,然后再定义一个自己的方法teach,返回值类型也是string;然后修改函数入参的类型声明;

...
interface Teacher extends Person {
  teach(): string
}
function getPerson(person: Teacher): void {
  console.log(person.name);
  
}

const person = {
  name: 'Dabin',
  height: 180,
  say(){
    return 'hello'
  }
}

getPerson(person) // 报错,缺少teach方法

可以看到,上面的函数调用又报错了,缺少teach方法,所以我们还需要给person加上teach方法;

...
const person = {
  name: 'Dabin',
  height: 180,
  say(){
    return 'hello'
  },
  teach() {
    return 'teach'
  }
}
...

可以看出,当一个接口继承了一个接口之后,他就拥有了这个接口的所有属性和方法,同时又还有自己的属性和方法;

同时也可以通过一个类去实现一个接口,那这个类必须包含接口里面的所有属性和方法;

interface Person {
  name: string,
  age?: number,
  [propName: string]: any,
  say(): string
}

class P implements Person {
  constructor(name: string) {
    this.name = name
  }
  name: string;
  say() {
    return 'hello'
  }
}

这样就通过一个类实现了一个接口了,上面implements就是类实现接口的关键字,他的中文意思也是实现的意思;上面的类中,必须要含有name属性和say方法,不然都会报错的;

也可以通过接口定义一个函数,具体实现如下

interface Hello {
  (word: string): string
}

const sayHi: Hello = () => {
  return 'hello'
}

上面的接口定义各一个函数,函数名是string,返回值也是string类型,如果将返回值类型更改成其他类型的话就会报错;

总结

以上就是我学习和使用TS中的一些语法总结啦,TS是一门很注重类型的编程语言,所以在使用过程中,我们一定要加强类型思想,当然,TS也帮我们做了一些类型推断,这样我们就能在编程中就发现一些错误,同时,给变量增加类型之后,他的一些方法和属性都会自动推荐出来,你会发现编程真的方便了很多;真的很赞!文章到这来就结束了,希望对你有帮助!