maicFir / lessonNote

JS学习笔记
33 stars 11 forks source link

TS扫盲基础 #33

Open maicFir opened 2 years ago

maicFir commented 2 years ago

最近半年项目升级 ts,一边看文档,一边实践,ts基础语法非常简单,但是写好ts就非常不简单,typescript严格来讲算是一门强类型语言,它赋予js类型体系,让开发者写js更加严谨,并且它具备强大的类型推断,并且能在node浏览器中运行。对于项目而言,使用 typescipt 对提升项目的规范与严谨性更加友好。

本文只做笔者项目中常遇到的一些ts经验,希望在项目中你能用得到。

正文开始...

定义常用类型

type[string|number|boolean|Array|Object|Function]

// string
type NameType = string;
const nameStr: NameType = 'Maic'; // const nameStr: string
//or
const nameStr2: string = 'tom';
// number
type AgeType = number;
const age: AgeType = 18;
// or
const age2: number = 20;
const age2: AgeType = ''; // const age: number 不能将类型“string”分配给类型“number”。
// Array<string>
type NamesType = Array<string>;
const students: NamesType = ['Maic', 'Tom'];
// or 等价于
type NamesType2 = string[];
const students2: NamesType2 = ['Maic', 'Tom'];

定义数组对象的类型

// 例如一个数组
/**
 const arr = [{
  name: 'Maic',
  age: 18,
  lovePlay: 'basketball'
}];
**/
// 如何定义该数组内部的类型
type itemArr = {
  name: string;
  age: number;
};
const arr: itemArr[] = [{ name: 'tick', age: 18 }];
console.log(arr[0].name);

类型推断和提示

定义对象类型

// Object
type Attrs = Object;
const personObj: Attrs = {};
// or
type nameObj = {
  name: string;
  age: number;
};
const personObj2: nameObj = {
  name: '大大',
  age: 18
};
console.log(personObj2.age);

定义函数类型

type Fn = Function;
const getAge: Fn = () => {}; // const getAge: Function

函数形参类型

type NameType = string;
function getName(name: NameType, age?: number) {
  return `我的名字是:${name}`;
}
// or
function getName(name: string, age?: number): string {
  return `我的名字是:${name}`;
}
getName('Maic');

注意以上第二个形参中?age:number代表这个形参可传可不传,并且这个函数返回的值类型是一个字符串

联合类型[string[] | number]

type idTypes = string[] | number;
const ids: idTypes = '123';
// or
const ids2: string | number = 123;
function getIds(id: idTypes, name?: string) {
  console.log(id.toString(), name);
  // console.log(id.join(',')); 如果不判断类型,则会直接提示
  // 类型“idTypes”上不存在属性“join”。
  // 类型“number”上不存在属性“join”。
  if (Array.isArray(id)) {
    console.log(id.join(','));
  }
}
getIds(['1', '2']);

toString这个方法在数组和number中都有该方法,所以可以使用,如果某个方法只存在于一种类型中,则要类型收窄判断该类型

interface 接口

interface我们可以理解它是定义对象的一种类型,并且它具备扩展对象属性,继承对象特征 在之前我们用type定义了对象数据

type nameObj = {
  name: string;
  age: number;
};
const personObj2: nameObj = {
  name: '大大',
  age: 18
};

interface定义对象

interface personObj2 {
  name: string;
  age: number;
}
const personObj4: personObj2 = {
  name: 'Maic',
  age: 18
};

如果我需要一个对象类型的属性是可选的

interface personObj2 {
  name: string;
  age?: number;
}

只需要在定义的属性后面加个?就行

extends 继承并扩展属性

// personObj5继承personObj2属性,所以personObj5具有personObj2的所有属性
interface personObj5 extends personObj2 {}
interface personObj5 extends personObj2 {
  money: number;
}
const personObj5: personObj5 = {
  name: 'Tom',
  money: 1000
};

type通过交集扩展属性

/*** type 通过交集扩展属性 */
type personObj6 = personObj2 & { money: number };
const personObj6: personObj6 = {
  name: 'Tom',
  money: 100
};

这里我们注意比较下typeinterfance的区别

相同点

所有对象类型都可以用type或者interface来定义,type在实际项目中更广义些,而interface更多的时候描述一个对象类型更狭义一些,他们都可以定义对象类型

不同点

type 定义好了的数据,不能重载,且扩展属性需要使用交集扩展&

interface可以重载,扩展属性需使用extends

type Animal = {
  name: string;
};
// 标识符“Animal”重复。ts(2300)
// type定义完了的类型,不能重复定义

// type Animal = {
//   age: string;
// }
// & 扩展属性
type NewAnimal = Animal & { age: number };
interface Dog {
  name: string;
}
interface Dog {
  age: number;
}
const dog: Dog = {
  name: '',
  age: 1
};
interface childDog extends Dog {
  money: string;
}
const cDog: childDog = {
  name: 'xx',
  age: 0.5,
  money: ''
};

as 类型断言

在项目中,如果你不知道该形参或者变量的类型,如果只是为了快点糊项目,不想被这个类型所拘束,那么你可以用as any

function $id(id) {
  return document.getElementById(id);
}
type elm = HTMLElement;
const dom: elm = $id('app') as HTMLElement;

联合类型

type namesType = string | number;
function getNames(name: namesType | 'Maic') {
  return name;
}
getNames('Maic'); // or getNames(123)

function handlequest(url: string, methods: string, params: Object) {
  fetch(url);
}
const options = {
  url: 'https://www.baidu.com',
  methods: 'get',
  params: {
    q: 'test'
  }
} as const;
handlequest(options.url, options.methods as 'get', options.params);

in 收窄类型

interface shopList {
  js: string;
  node: string;
}
function printShop(books: shopList) {
  if ('js' in books) {
    console.log(`我买了 ${books['js']}`);
  }
  if ('node' in books) {
    console.log(`这是一本 ${books['node']}书籍`);
  }
}
printShop({ js: 'js设计模式', node: 'nodejs入门到放弃' });

将一个的enums值的value做为另一个对象的key,将一个枚举值的key作为一个对象的value

const enum FOODS {
  a = '鸭子',
  b = '鸡腿'
}
console.log(FOODS.a);
type values = keyof typeof FOODS; // type values = "a" | "b"
const foods: {
  [key in FOODS]: values;
} = {
  [FOODS.a]: 'a',
  [FOODS.b]: 'b'
};
/**
 * const foods: {
    鸭子: "a" | "b";
    鸡腿: "a" | "b";
}
 */
console.log(foods[FOODS.a]);

instanceof 收窄

我们可以用instanceof收窄数据类型

function transformParams(params) {
  if (params instanceof String) {
    console.log(params.toLocaleLowerCase());
  }
  if (params instanceof Date) {
    console.log(params.toLocaleDateString());
  }
}
transformParams('abc');
transformParams(new Date());

is 判断

我么判断一个形参是否在一个类型中

/**
 * is 判断参数类型
 */
interface Fish {
  swim: Function;
}
interface Bird {
  fly: Function;
}
function isFish(arg: Fish | Bird): arg is Fish {
  return (arg as Fish).swim !== undefined;
}
const isfish = isFish({ swim: () => {} });
const isBird = isFish({ fly: () => {} });

rest params

可以跟es6一样...扩展多个参数

/**
 * rest params
 */
function add(num: number, ...arg: number[]) {
  return arg.map((s) => s + num);
}
add(1, 2, 4, 5, 6); // [3,5,6,7]
interface params {
  id: number;
  name: string;
  age: number;
  fav: string;
}
const curentParams: params = { id: 1, name: 'Maic', age: 18, fav: 'play' };
const { id, ...arg } = curentParams;
/*
const arg: {
  name: string;
  age: number;
  fav: string;
}
*/

可选属性[?]or 只读属性[readonly]

我们想一个对象的属性可有可无,或者一个对象属性不能修改

/***
 *
 * 对象属性修饰符  ? 可选  readonly 只读
 */
interface params2 {
  readonly id: number;
  name: string;
  age?: number;
}
const curentParams2: params2 = { id: 123, name: '' }; // age 可有可无
// curentParams2.id = 456; // 无法分配到 "id" ,因为它是只读属性。 readonly id的属性不能修改

对象索引类型

通常我们一个对象的key是字符串或者是索引,那么正确定义对象索引的类型就如下面

/**
 * 对象属性索引类型
 */
interface params3 {
  [key: string]: string | number;
  [key: number]: number;
}
const params3: params3 = {
  age: 18,
  1: 123
};

如果我需要将一个对象key声明成另一个对象的key呢?那么我们可以使用[key in xxx]: string

enum LANGUAGE {
  ru = '俄罗斯',
  ch = '中国',
  usa = '美国'
}
type languKey = keyof typeof LANGUAGE; // type languKey = "ru" | "ch" | "usa"
/**
 * const lang: {
    ru: string;
    ch: string;
    usa: string;
}
 */
const lang: {
  [key in languKey]: string;
} = {
  ru: '1',
  ch: '2',
  usa: '3'
};

交叉类型 x & b

/**
 * 交叉类型
 */
interface span {
  color: string;
}
interface a {
  cursor: string;
}
type divType = span & a;

const divStyle: divType = {
  color: '#111',
  cursor: 'pointer'
};
console.log(divStyle.color, divStyle.cursor);

注意我们使用extends也一样可以一样的效果

interface a {
  cursor: string;
}
// img 类型同时拥有cursor与{color: string}两个属性类型
interface img extends a {
  color: string;
}
const imgStyle: img = {
  color: '#111',
  cursor: 'pointer'
};

利用泛型复用 interface

通常在实际业务中, 通用的属性值可能类型不同那么我们会重复定义很多类型,比如下面

interface obj1 {
  a: boolean;
}
interface obj2 {
  a: string;
}
const obj1: obj1 = { a: true };
const obj2: obj2 = { a: '111' };

因此我们可以这么做

// 将多行类型合并成一个
interface objType<T> {
  a: T;
}
const obj3: objType<boolean> = {
  a: false
};
const obj4: objType<string> = {
  a: 'hello'
};

当我们看到interface objType<T> { a: T },我们怎么理解,首先objType你可以把它看成一个接口名称,其实与普通申明一个普通接口名一样,T可以看成一个形参,一个占位符,我们可以在实际用的地方灵活的传入不同类型。

type 泛型复用

// Type泛型
interface obj2 {
  a: string;
}
type obj4Type<Type> = {
  content: Type;
};
const obj5: obj4Type<obj2> = {
  content: {
    a: 'hello'
  }
};
console.log(obj5.content.a); // hello

方法泛型复用

通常我们在项目中经常看到封装的工具函数中有泛型,那么我们可以简单的写个,具体可以看下下面简单的一个一个工具请求函数

/***
 *
 * 方法泛型
 */
function genterFeach<T>(url: string) {
  return {
    get: (params: T, config?) => {
      return fetch(url, {
        method: 'get',
        body: JSON.stringify(params),
        ...config
      });
    },
    post: (params: T, config?) => {
      return fetch(url, {
        method: 'post',
        body: JSON.stringify(params),
        ...config
      });
    }
  };
}
interface paramsF {
  id: number;
  password: number;
  name: string;
}
const useInfo = genterFeach<paramsF>('/v1/useInfo');
const login = genterFeach<paramsF>('/v1/login');
useInfo.get({ id: 111, password: 12, name: 'Maic' });
login.post({ id: 111, password: 12, name: 'Maic' });

readOnly 只读

/**
 * readonly
 */
type readData = readonly [string, number];
const data: readData = ['Maic', 18];
// data[1] = 20; 无法分配到 "1" ,因为它是只读属性
type readData2<T> = T;
const data2: readData2<readonly string[]> = ['Maic'];
// data2[0] = 'tom';// 类型“readonly string[]”中的索引签名仅允许读取
console.log(data2[0]);

[xx,xx,xx] as const

内部元素会变成一个常量,不可修改

const strArr = ['a', 'b', 3] as const;

type strVal = typeof strArr;

const strArr2: strVal = ['a', 'b', 3];

function getStrArr([a, b, c]: [string, string, number]) {
  console.log(a, b, c);
}
// getStrArr(strArr);// 类型 "readonly ["a", "b", 3]" 为 "readonly",不能分配给可变类型 "[string, string, number]"。

function getStrArr2([a, b, c]: strVal) {
  console.log(a, b, c);
}
getStrArr2(strArr); // ok

泛型

对于泛型在笔者初次遇见她时,还是相当陌生的,感觉这词很抽象,不好理解,光看别人写的,一堆泛型,或许增加了阅读代码的复杂度,但是泛型用好了,那么会极大的增加代码的复用度。当然,简单事情复杂化了,那么泛型也容易出错,代码也变得不易读。

我们写一个简单的例子来感受一下泛型

interface resopnseID {
  id: number;
}
interface responseName {
  name: string;
}
const responseId: resopnseID = {
  id: 123
};
const responseName: responseName = {
  name: 'Maic'
};

如果我想resopnseID或者responseName高度复用呢,如果有很多类似的字段,那么我是不是要写很多这种接口类型呢

interface keysType<T, V> {
  [key in T]: V;
}
const responseId2: keysType<{ id: number }, number>;
const responseName2: keysType<{ name: string }, string>;
console.log(responseName2.age); // 类型“keysType<{ name: string; }, string>”上不存在属性“age”。
console.log(responseName2.name);
// 函数泛型
function setParamsType<T>(arg: T): T {
  return arg;
}
console.log(setParamsType<string>('maic'));
console.log(setParamsType<number>(18));

与下面等价,可以用interface申明函数类型

// 接口泛型
interface paramsType<T> {
  [arg: T]: T;
}
function setParamsType<T>(arg: T): T {
  return arg;
}
const myParams: parsType<number> = setParamsType;
// type 泛型
type parsType2<T> = {
  [arg: T]: T;
};
const myParams2: parsType2<number> = setParamsType;

类泛型

我们在用class申明类时,就可以约定类中成员属性的类型以及class内部方法返回的类型

class Calculate<T> {
  initNum: T;
  max: string;
  add: (x: T, y: T) => T;
}
const cal = new Calculate<number>();
cal.initNum = 0;
cal.add = (x, y) => x + y;
cal.add(1, 2);
// or
const cal2 = new Calculate<string>();
cal.max = '123';
cal.add = (x, y) => x + y;
cal.add(cal.max, '456'); // 123456

约束泛型

在平时项目中我们使用泛型,我们会发现有时候,函数内部使用参数时,往往会提示属性不存在,比如

// 类型“T”上不存在属性“id”。
function getParams<T>(params: T) {
  if (params.id) {
    console.log('进行xxx操作');
  }
}
getParams({ id: '123' });

此时我们就可以利用extends约束泛型做到函数内部能正确访问

function getParams<T extends { id: string }>(params: T) {
  if (params.id) {
    console.log('进行xxx操作');
  }
}
getParams({ id: '123' });
interface parmasType {
  id: string;
}
function getParams2<T extends parmasType>(params: T) {
  if (params.id) {
    console.log('进行xxx操作');
  }
}

接下来看一段原型属性推断与约束,我们可以看出构造函数与实例的关系

class Animal2 {
  name: string = 'animal';
}
class Sleep {
  hour: number = 10;
}
class Bee extends Animal2 {
  age: number = 1;
  action: Sleep = new Sleep();
}
function createInstance<T extends Animal2>(c: new () => T): T {
  return new c();
}
console.log(createInstance(Bee).action.hour); // animal

keyof

我们对一个对象类型接口进行keyof那么会返回对象属名组成的集合

interface keysObj {
  id: string;
  name: string;
  date: string | number;
  content: string;
}
type keytype = keyof keysObj;
// 等价于type keytype = 'id' | 'name' | 'date' | 'content'
const objkey: keytype = 'content';
// or
const objkey2: keyof keysObj = 'id';
// 简写
// const objkey: keyof keysObj = 'content'
interface keysP {
  [key: number]: string;
}
type keysType3 = keyof keysP; // type keysType3 = number
const objkey3: keyof keysP = 1;

如何获取一个对象值的所有key

const objkey4 = {
  a: '111',
  b: '222',
  c: 333,
  d: 444
};
type result = keyof typeof objkey4; // type result = "a" | "b" | "c" | "d"
const objkey5: result = 'a'; // true

通过keyof我们已经约束了一新值的所有值,那么它就再也不能赋值其他值了,比如

...
const objkey5:result = 'e'; // error 不能将类型“"e"”分配给类型“"a" | "b" | "c" | "d"”

Extract

当我们对一个对一个泛型进行keyof时,此时类型会变成string | number | symbol三种类型,我们对变量进行赋值时,ts会报错

function useKey<T, Key extends keyof T>(o: T, key: Key) {
  const keyName: string = key; // 不能将类型“string | number | symbol”分配给类型“string”
  console.log(o[keyName]);
}

那么此时我们需要借助Extract进一步进行约束

function useKey2<T, Key extends Extract<keyof T, string>>(o: T, key: Key) {
  const keyName: string = key;
  console.log(o[keyName]);
}

注意我们看下ts全局给我们提供的这个Extract类型

type Extract<T, U> = T extends U ? T : never;

我们观察到Extract就是约束了Key的类型,那么我们也可以这么写,既然我知道Key是字符串

function useKey3<T, Key extends string>(o: T, key: Key) {
  const keyName: string = key;
  console.log(o[keyName]);
}

或者你也可以用或类型,指定keyName可以是string | number | symbol这三种类型

function useKey4<T, Key extends keyof T>(o: T, key: Key) {
  const keyName: string | number | symbol = key;
  console.log(o[keyName]);
}

typeof

typeof只能用于已经实际定义申明了的变量,返回该定义的变量的实际类型

let publicWebTech = '关注公众号:Web技术学苑';
type publicWeb = typeof publicWebTech;
//type publicWeb = string
const publicName: publicWeb = '';

但是注意如果用const定义的变量,如果你keyof一个常量,那么就会不一样了

const publicWebAuthor = 'Maic';
// or let publicWebAuthor = 'Maic' as const;
type publicWebAuthor = typeof publicWebAuthor;
const publicAuthor: publicWebAuthor = 'Maic';

获取一个对象的所有属性类型

const objResult = { a: '11', b: '222' };
type objResultType = typeof objResult;
/*
  type objResultType = {
    a: string;
    b: string;
  }
*/

获取一个函数的返回类型,ReturnType

function fnTest() {
  return {
    a: '111',
    b: '222'
  };
}
type fntest = ReturnType<typeof fnTest>;
/**
  type fntest = {
    a: string;
    b: string;
  }
 **/

我们可以看下ReturnType的类型定义

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

有时候我们定义一个枚举,我们想获取枚举的Key,那么可以这么做

enum SERVER {
  TEST = 1,
  PRD = 2,
  DEV = 3
}
type serverType = keyof typeof SERVER;
// type serverType = "TEST" | "PRD" | "DEV"

访问索引类型

有时我们需要访问具体接口的某个字段的类型或者数组中的类型

interface person {
  name: string;
  id: number;
  age: number;
}
type nametype = person['age'];
// type nametype = number
type nameOrAge = person['age' | 'name'];
// type nameOrAge = string | number

type personKeys = person[keyof person];
// type personKeys = string | number

数组中的类型

const personArr = [
  {
    name: 'Maic',
    age: 10
  },
  {
    name: 'tom',
    age: 18,
    id: 189
  }
];
type items = typeof personArr[number];
/*
type items = {
    name: string;
    age: number;
    id?: undefined;
} | {
    name: string;
    age: number;
    id: number;
}
*/

条件类型 extends

// 类型“"message"”无法用于索引类型“T”。
type messageOf<T> = T['message'];

此时可以用extends

type messageOf<T extends { message: string }> = T['message'];
type isNumber<T> = T extends number ? number : string;
const num: isNumber<string> = '123';
// const num: string

总结

1、在ts定义基础数据类型,typeinterface

2、基础使用泛型,可以在接口,函数,type使用泛型,泛型可以理解js中的形参,更加抽象和组织代码

3、extends约束泛型,并且可以在ts中做条件判断

4、使用keyof获取对象属性key值,如果需要获取一个对象定义的key,可以使用type keys = keyof typeof obj

5、有一篇笔者很早之前的一篇ts 笔记

6、本文示例code-example

更多学习ts查看TS 官方文档,也可以看对应翻译中文版https://yayujs.com/