总结在使用TS中的一些实用技巧

526 阅读4分钟

使用TS已有半年之久,年底了,总结下项目中,个人使用TS的一些心得技巧,欢迎留言吐槽。

1. 参考lodash实现的 get 方法:


function get<T extends Object, Key extends keyof T>(obj: T, key: Key  ) { 
  return obj[key];
}

2. set 方法实现:


function set<T extends Data, Key extends keyof T,>(obj: T, key: Key, value: T[Key]) { 
  obj[key] = value;
  return obj;
}

可以基于set的实现,统一处理表单数据,简化设置state的代码,以下是在React中的运用:

3. async await 的TS 封装

await-to-js 中学到的异步优雅封装。 当我们发起异步请求,(例如接口请求),对于成功和失败的数据。 该函数统一返回一个数组,

  • 第一个元素表示错误,成功返回null, 失败返回 继承自Error的 对象,
  • 第二个参数表示Promise.resolve() 的成功的数据, 失败返回null.
export function awaitTo<T, U = Error> ( promise: 
Promise<T>, errorExt?: object): Promise<[U, undefined] | [null, T]> {

  return promise.then<[null, T]>((data: T) =>
  [null, data]).catch<[U, undefined]>((err: U) => {
      if (errorExt) {
        Object.assign(err, errorExt);
      }
      return [err, undefined];
    });
}

// 使用方法:
  const [error, dataList] = await awaitTo<DataResponse>(promiseObj);

深度Partial

Partial作用是将传入的属性变为【可选项】,不过TS内置的只支持第一层,第二层以后就不会处理了, 如果要处理多层,就只能自定义DeepPartial. 依靠递归来解决。如下:

export type DeepPartial<T> = {
  [key in keyof T]? : T[key] extends Object ? DeepPartial<T[key]> : T[key]
}

any 和 unknow

  • any 类型本质上是类型系统的一个逃逸舱。完全忽略typescript检查,简单粗暴。
  • unknow 是 ts3.0版本新增特性,和any相比,更加严格。 unknown 类型只能被赋值,或给 any 类型和 unknown 类型本身. 有些同学会好奇,什么时候用any,什么时候用unkonw ? 以下是我个人的总结:
any使用场景:
  1. 历史JS 项目迁移
  2. 调用引用某第三方的,类和实例,未生命TS类型定义(慎之又慎)

unknow 使用场景,看示例:

let value: unknown;

value = true;             // OK
value = 42;               // OK
value = "Hello World";    // OK
value = [];               // OK
value = {};               // OK
value = Math.random;      // OK
value = null;             // OK
value = undefined;        // OK
value = new TypeError();  // OK
value = Symbol("type");   // OK
let value: unknown;

value.foo.bar;  // Error
value.trim();   // Error
value();        // Error
new value();    // Error
value[0][1];    // Error

如果需要对unknow 类型,做如下操作:

let value: unknown;
value ='1&2';  // ok
value.split('&')  // error 

虽然 value 赋值成功,但value.splice 却未通过编译。如果明确类型,可以通过 as(类型断言) 来处理.

(value as string).split('&')  // ok 

断言!

! 的作用是断言某个变量不会是 null/undefined,告诉编辑器停止报错。


const obj = {
    name: 'loy'
}
const a = obj!.name; // 假设 obj是你从后端获取的获取

确定 obj.name 一定是存在的且不是null/undefined,使用! 只是消除编辑器报错,不会对运行有任何影响。

属性或者参数中使用 !,表示强制解析(告诉 typescript 编译器,这里一定有值); 变量后使用 !: 表示类型推荐排除 null/undefined。

再看以下例子;

function myFunc(maybeString: string | undefined | null) {
  // Type 'string | null | undefined' is not assignable to type 'string'.
  // Type 'undefined' is not assignable to type 'string'. 
  const onlyString: string = maybeString; // Error
  const ignoreUndefinedAndNull: string = maybeString!; // Ok
}

?? 空值合并

在 TypeScript 3.7 版本中除了引入了前面介绍的可选链 ?. 之外,也引入了一个新的逻辑运算符 —— 空值合并运算符 ??。当左侧操作数为 null 或 undefined 时,其返回右侧的操作数,否则返回左侧的操作数。

与逻辑或 || 运算符不同,逻辑或会在左操作数为 falsy 值时返回右侧操作数。也就是说,如果你使用 || 来为某些变量设置默认的值时,你可能会遇到意料之外的行为。比如为 falsy 值(''、NaN 或 0)时。

这里来看一个具体的例子:


// 以前的处理方式, 给默认值

 const params = params || 'defalut value'; 
console.log('params: ', params);

// 我们希望如果参数是 null 或者 undefined 才赋值 默认参数。, 用空值合并运算符 来解决

const foo = null ?? 'default string';
console.log(foo); // 输出:"default string"

const baz = 0 ?? 42;
console.log(baz); // 输出:0

以上 TS 代码经过编译后,会生成以下 ES5 代码:

"use strict";
var _a, _b;
var foo = (_a = null) !== null && _a !== void 0 ? _a : 'default string';
console.log(foo); // 输出:"default string"

var baz = (_b = 0) !== null && _b !== void 0 ? _b : 42;
console.log(baz); // 输出:0

通过观察以上代码,我们更加直观的了解到,空值合并运算符是如何解决前面 || 运算符存在的潜在问题。下面我们来介绍空值合并运算符的特性和使用时的一些注意事项。

当空值合并运算符的左表达式不为 null 或 undefined 时,不会对右表达式进行求值


function A() { console.log('A was called'); return undefined;}
function B() { console.log('B was called'); return false;}
function C() { console.log('C was called'); return "foo";}

console.log(A() ?? C());
console.log(B() ?? C());

// 上述代码运行后,控制台会输出以下结果:
/* 
  A was called 
  C was called 
  foo 
  B was called 
  false 
*/

如何将外部类型导入全局 .d.ts 文件

使用以下任一方法:

interface Test {
  date: import('moment').Moment;
}

或者

type Moment = import('moment').Moment;
interface Test {
  date: Moment;
}

注意: 当文件具有顶级importexport语句时,它将被视为一个模块。不再作为全局模块。