likes
comments
collection
share

TS个人总结(一)

作者站长头像
站长
· 阅读数 3

TS个人知识总结(一)

TS类型分类

在ts中类型一分为原始数据类型和对象类型:

const a:string = 'aa'
const b:number = 123
const c: boolean = true
const d:null = null
const e:undefined = undefined

默认情况下nullundefined是所有类型的子类型,可以把nullundefined赋值给其它任何类型:

// null 和 undefined 赋值给 number
let num: number = 1;
num = null;
num = undefined;
 
// null 和 undefined 赋值给 boolean
let bool: boolean = false;
bool = null;
bool = undefined;

// null 和 undefined 赋值给 object
let obj: object = {};
obj = null;
obj = undefined;

对于原始类型的标注很简单可以用: 类型类进行实现,这里的类型也就是string/number/boolean:

const userName: string = 'linbudu';
const userAge: number = 18;
const userMarried: boolean = false;

然而对于对象类型是数组类型的类型描述却不同:

数组:

let arr :string[] = ['a','b','c']  //元素全部是字符串数组
let arr1: number[] = [1,2,3]  //元素全部是数字的数组
let arr2: boolean[] = [true,false,true] //元素全部是布尔值的数组
let arr3: Object[] = [{'name':'a'}]  //元素是对象的数组

对于数字类型标注还有一种方式 let arr4:Array<string>  这种方式和上面的

对象:

对于对象类型的描述我们需要用关键词interface来进行类型约束:

let obj={
  name:'小明',
  age:12,
  sex:'男'
  isFun:true
}

对于上面对象的类型描述
interface UserInfo{
  name:string,
  age:number,
  sex:string,
  isFun:boolean
}

let obj:UserInfo={
  name:'小明',
  age:12,
  sex:'男'
  isFun:true
}

当然如果是对象肯定会出现对象的属性是一个对象,当你想要对这个属性对象进行类型描述时:

let obj = {
  name:'小明',
  age:12,
  sex:'男'
  isFun:true,
  info:{
     class:'3年2班',
     score:80
  }
}

这时肯定会有些大聪明一波流rush b
其实在这里有更好的方法
interface InfoData{
   class:string,
   score:number
}


interface UserInfo{
  name:string,
  age:number,
  sex:string,
  isFun:boolean,
  info:InfoData
}

let obj:UserInfo = {
  name:'小明',
  age:12,
  sex:'男'
  isFun:true,
  info:{
     class:'3年2班',
     score:80
  }
}

而接口加上数组类型,就可以描述一个成员是对象的数组类型:

let userList:UserInfo[] = [
 {
  name:'小明',
  age:12,
  sex:'男'
  isFun:true,
  info:{
     class:'3年2班',
     score:80
  }
}, 
{
  name:'小红',
  age:11,
  sex:'女'
  isFun:true,
  info:{
     class:'3年2班',
     score:90
  }
},
 {
  name:'小波',
  age:12,
  sex:'男'
  isFun:true,
  info:{
     class:'3年2班',
     score:70
  }
}
]

当一个对象被接口约束类型之后代码中的赋值需要完全符合这个接口定义的接口:

必须拥有所有接口中定义的属性,不能多也不能少

interface User{
   name:string,
   age:number
}

let user1:User = {
     name:'小明',
     age:20
}

//此时ts是不会报错的

let user2:User = {
     name:'小明',
     age:20,
    // 对象字面量只能指定已知属性,并且“userJob”不在类型“User”中。
     sex:'男'
}

//此时ts会报错因为在接口类型中没有sex

//假如我需要将sex类型变为可选类型可以在接口中定义可选标记
interface User{
   name:string,
   age:number,
   sex?:string
}
   
 //此时定义user1和user2都不会报错 因为此时sex是一个可写可不写得属性

上面这些例子中,本质上都是使用对象来存放一个“值”,即这个对象描述的是一组具体的信息:你的用户名、你的年龄等。而在 JavaScript 中,我们还经常使用对象来存放常量:

const userLevelCode = {
  Visitor: 10001,
  NonVIPUser: 10002,
  VIPUser: 10003,
  Admin: 10010,
  // ... 
}

这样我们能清楚知道Visitor,NonVIPUser等这些是有什么相关的属性

这么做的好处是我们能够避免项目中出现 Magic Value,即莫名其妙的一个值,没有任何的注释,只能靠猜来理解这到底是个什么玩意:

fetchUserInfo({
  // ...
  // 后续维护者:这到底是个啥??
  userCode: 10001
})

fetchUserInfo({
  // ...
  // 后续维护者:哦,这里要给访客用户啊
  userCode: userLevelCode.Visitor
})

而在 TypeScript 中则提供了一个更好的常量定义方式,即枚举,上面的例子用枚举改写后是这样的:

enum UserLevelCode {
  Visitor = 10001,
  NonVIPUser = 10002,
  VIPUser = 10003,
  Admin = 10010,
  // ... 
}

看起来好像只是换了个写法,但其实枚举能带来更明显的好处,首先最重要的就是,相比于使用对象,枚举能够提供清晰的提示,甚至可以看到这个枚举成员的值

常用例子:

当我们在api文件中去调用接口时,他的接口路径我们可以将相关联的接口放在一起这样让代码更加清晰

enum  Api= {
  userInfo:`/user/info/${id}`,
  userScore:`/user/score`,
  userMessage:`/user/message`
} 

这样你能清晰知道那些接口是用户的,方便后期维护

同时,对于这种数字类型的值,枚举能够自动累加值:

enum UserLevelCode {
  Visitor = 10001,
  NonVIPUser,  //此时没有赋值时对于枚举中的数字类型是默认在上一个属性的值上+1  也就是10002
  VIPUser, // 10003
  Admin = 10010,
  // ...
}

由于 NonVIPUser 和 VIPUser 是直接累加的,因此这里我们可以省略赋值,由枚举自动地进行累加,以此用更少的代码来保持一致的功能性。

最后,枚举中可以同时支持数字、字符串、函数计算等成员:

function generate() {
  return Math.random() * 10000;
}

enum UserLevelCode {
  Visitor = 10001,
  NonVIPUser = 10002,
  VIPUser,
  Admin,
  Mixed = 'Mixed',
  Random = generate(),
  // ...
}

对于es6中的map和set数据类型进行类型约束

let set = New Set<number>()

set.add(1);
set.add('2'); // X 类型“string”的参数不能赋给类型“number”的参数。


const map = new Map<number, string>();
//约束key只能是number类型 value只能是srting类型
map.set(1, '1');
map.set('2', '2'); // X 类型“string”的参数不能赋给类型“number”的参数。

函数类型

首先,在 JavaScript 中,函数有函数表达式与函数声明两种写法:

const handler = function (args) {}; // 函数表达式
const handler = (args) => {}; //  箭头函数表达式

function handler(args) { }; // 函数声明

函数声明与函数表达式下的类型描述:

function sum(a: number, b: number): number {
  return a + b;
}

const sum = function(a: number, b: number): number {
  return a + b;
};

对于函数表达式写法,你可能会想,能不能使用 const sum: 函数类型 = 的方式进行类型标注,保持像变量类型标注的语法一样?

是可以的,声明一个类型别名:Sum

type Sum = (a: number, b: number)=>number

type Sum = 的语法称为类型别名,我们先不用理解它到底是个什么语法,只要了解它能用来给一个类型起一个新名字,比如:

type MagicString = string; // 一个神奇字符串

需要注意的是,使用类型别名保存函数类型时,我们的写法是 (a: number, b: number) => number; ****而不是 (a: number, b: number): number;。现在我们就可以使用这个类型来作为函数表达式类型了:

type Sum = (a: number, b: number) => number;

const sum: Sum = function(a, b) {
  return a + b;
};

发现我们不用去在函数中去一个个定义参数的类型了,这是因为ts通过函数类型推导出该函数的参数类型和返回值类型了

但 TypeScript 中的函数类型并不能完全套用 JavaScript 中的概念,最明显的一个例子就是对于无返回语句的函数返回值类型描述。我们知道,在 JavaScript 的函数中,如果没有显式的 return 语句,那么这个函数的执行结果实际会是 undefined,但在 TypeScript 中,我们需要将这个函数的返回值类型标注为 void 而不是 undefined:

function handler1(): void {}; // √
function handler2(): undefined {}; // X

如果你希望函数返回的是一个undefined:

const normal = ():void=>{
return
}

这是因为在 TypeScript 中,undefined 也被视为一个有意义的类型。因此如果你希望将返回值类型标注为 undefined,就需要有显式的 return 语句

除了类型标注以外,在 TypeScript 的函数还带来了一位新朋友-函数重载,这也是相当有必要了解的一个概念。

不妨先来想象一个场景,在 JavaScript 中,如果一个函数可能存在多种入参组合,比如我们有一个 sum 函数,它接受两个参数,基于参数类型的不同,它会执行不同的逻辑并返回不同的值:

  • 入参均为数字类型时,相加这两个参数,如 sum(1, 2) 返回 3。
  • 一个参数为数字类型数组,另一个参数为数字类型时,让数组参数中的每个数字加上数字参数,再返回这个数字,如 sum([1, 2, 3], 4) 和 sum(4, [1, 2, 3]) 返回 **[5, 6, 7]
  • 如果两个参数是长度一致的数字类型数组时,依次相加每个数字,返回相加后的数组,如 sum([1, 2, 3], [4, 5, 6]) 返回 **[5, 7, 9]

完整的实现与示例如下:

function sum(x, y) {
  if (typeof x === 'number' && typeof y === 'number') {
    return x + y;
  } else if (Array.isArray(x) && typeof y === 'number') {
    return x.map((num) => num + y);
  } else if (typeof x === 'number' && Array.isArray(y)) {
    return y.map((num) => num + x);
  } else if (Array.isArray(x) && Array.isArray(y)) {
    if (x.length !== y.length) {
      throw new Error('Arrays must have the same length');
    }
    return x.map((num, index) => num + y[index]);
  } else {
    throw new Error('Invalid arguments');
  }
}

console.log(sum(2, 3)); // 5
console.log(sum([1, 2, 3], 4)); // [5, 6, 7]
console.log(sum(5, [1, 2, 3])); // [6, 7, 8]
console.log(sum([1, 2, 3], [4, 5, 6])); // [5, 7, 9]
console.log(sum('a', 'b')); // Error: Invalid arguments
console.log(sum([1, 2, 3], [4, 5])); // Error: Arrays must have the same length

这其实就是函数重载的概念,它指的就是根据不同的入参匹配不同的实际逻辑,实现一个函数名走天下。但理想很美好,现实就比较忧伤了。在 JavaScript 中,此时为了尽可能描述清楚各个入参的作用,我们会这么写参数名:

function sum(base: number, incre: number): number;
function sum(baseArray: number[], incre: number): number[];
function sum(incre: number, baseArray: number[]): number[];
function sum(baseArray: number[], increArray: number[]): number[];
function sum(x: number | number[], y: number | number[]): number | number[] { }

需要注意的是,在标注了每一种可能的重载的方式以后,在最后那个实际实现的函数类型标注里,我们需要标注各个参数类型和返回值类型,使用上面所有重载可能出现的类型组成的联合类型。但实际上这最后一个函数类型标注并不会被调用方看到,在匹配到对应的调用时,我们就能够获取到与参数组合完全匹配的提示与类型保障, 你可能会发现,虽然类型层面做了重载,但好像函数内部还是需要自己通过朴素的 if else 判断当前是哪个参数组合...,这是因为,TypeScript 中的函数重载还是属于伪重载,它只能在类型层面帮你实现重载的效果,而实际的逻辑运行,由于 JavaScript 不支持,它也就束手无策了

函数类型重载个人感觉还是慎用

钙片总结

  • 原始类型和对象类型
  • 对象类型定义: 数组 对象接口(interface)
  • 可选值
  • 枚举
  • 类型别名 type
  • 函数类型定义(函数类型重载)