Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TS扫盲基础 #33

Open
maicFir opened this issue Aug 11, 2022 · 0 comments
Open

TS扫盲基础 #33

maicFir opened this issue Aug 11, 2022 · 0 comments

Comments

@maicFir
Copy link
Owner

maicFir commented Aug 11, 2022

最近半年项目升级 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/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant