likes
comments
collection
share

你知道TypeScript这些姿势么

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

前话

「TypeScript」赋予了「JavaScript」类型定义的能力,让JS既能保留动态类型编程语言的开发体验,又能享受到静态类型编程语言的编译时严谨的类型检查。

新人刚学TypeScript时,往往熟悉基本的类型定义、接口声明、继承多态和泛型后就上手TS开发了,在项目中简单的限定对象类型或者定义函数输入输出,遇到复杂的类型嫌麻烦直接就粗暴使用「any」,极端情况下甚至把TypeScript项目直接变成了「AnyScript」。

这显然违背了TypeScript的初衷,也未能充分正确使用TypeScript提供的能力。

记住哈,如果你决定使用TypeScript作为项目主开发语言,请尽可能给变量等赋予正确的类型。

本篇仅作为本人学习官方文档后的个人理解,如有出错还望大佬们指出改正。

实际上TypeScript有很多「骚姿势」帮助我们写完整的类型正确的代码,只是我们可能不了解他们,而本篇就简单抛砖引玉,把一些实际项目中用得上的写下来。

下面开始正文。

正文

1. 用 any 还是 unknown ?

TypeScript刚发布时,就提供了「any」类型,一个可变变量如果声明类型为any类型,那么它可以接受「任何类型」的值,如下:

let val: any;
val = 1; // 接受「number」类型变量
val = 'a'; // 接受「string」类型变量
val = () => console.log('hello world'); // 接受「Function」类型变量

但是同样的,因为是「任何值」,因此它做任何事情ts在编译时都认为它是对的,因此容易导致运行时出现异常报错,如下:

let val: any;
val = 1;
val.hello(); // 这里编译时不会报错,但运行时会抛出异常

所以如果想用一个变量接受任何值,但是又不想被「百分百信任」导致写出了bug,我们更好的方式是去使用「unknown」类型。

「unknown」类型跟「any」类型最大的不同是,对一个「unknow」变量,你想使用它做一些事情之前,你得「判断它的类型」或者「断言它的类型」。

function fn(val: unknown) {    
    val.toLowerCase(); // ts类型检查提示错误,编译时异常
    val(); // ts类型检查提示错误,编译时异常
}

1.1 判断它的类型

「判断它的类型」很好理解,代码演示如下:

function fn(val: unknown) {    
    if (typeof val === 'string') { // 判断为「string」类型
        console.log(val.toLowerCase());
    } else if (typeof val === 'function') { 判断为「function」类型
        val();
    }
}

1.2 断言它的类型

「断言」也就是在代码逻辑中强制将「unknown」转换为已知的类型。

这是合理的,因为有些情况我们是确定它的类型的,如下:

let val: unknown;
val = 'hello world';

const str = val as string; // 将val断言为「string」类型
console.log(str);

在「给变量断言」后,ts编译器会「百分百相信」,因此编译时直接通过检查。因此「断言」是个会出错的导致写bug的动作(编译通过,运行时异常),在「断言」过程中需要多加注意。

1.3 小结

到这里读者应该大致了解了「any」和「unknow」的区别,因此项目中如果要使用一个「万能」类型,尽可能偏向于使用「unknown」。

「unknown」对比「any」,「unknown」类型范围更小,更能发挥TypeScript的「类型限定」的能力。

这里不是禁用「any」类型,「any」也是个重要的类型,在某些场合发挥不可替代的作用,例如新人们一开始使用它的初衷——「类型极为复杂的实在难以定义,但是我知道它可以用来XXX」

2. void 和 never 区别?

之所以同时讲解他们是因为它们具有相似之处。

2.1 作为变量类型

作为变量类型的时候他们都为「空」,但是在变量接受上面存在区别,「void」能接受「undefined」值,但是「never」只能接受「never」,代码演示如下:

let a: void;
a = undefined; // 编译正常

let b: never;
b = undefined; // 类型检查报错

let c: never;
c = b; // 编译正常

在正确使用场景上来说「never」类型的变量不存在,通过「enum」类型代码演示如下:

enum EType {
  "a" = "a",
  "b" = "b",
}

function fn(type: EType) {
  switch (type) {
    case EType.a:
      break;
    case EType.b:
      break;
    default:
      // 这里的val变量类型为「never」
      // 因为case已经处理了所有枚举值的场景,若代码执行到default应为异常场景
      const val = type; 
      break;
  }
}

2.2 作为返回值类型

返回值为「void」的函数实际能返回任何值,但接受返回值的变量类型为「void」。这是合理的,因为接受返回值的变量并不能「做任何事情」,如下:

type IFn = () => void;
const fn1: IFn = () => {
  return; // 编译正常
};
const t1 = fn1(); // 值类型为「void」

const fn2: IFn = () => {
  return "Hello"; // 编译正常
};
const t2 = fn2(); // 值类型为「void」
console.log(t2.toLowerCase()); // 类型检查报错

返回值为「never」的场景只存在于「死循环」或者「抛出异常」的场景,代码讲解如下:

type IFn = () => never;

/** 死循环函数 **/
const fn1: IFn = () => {
  while (true) {
    // 编译正常
  }
};

/** 抛异常函数 **/
const fn2: IFn = () => {
  throw new Error(); // 编译正常
};

上面的代码很好解释了前面说的「never」类型的变量「不存在」。

3. 如何获取类型的子属性类型?

首先说明下这个小标题是什么意思?实际开发中存在这么一个场景,使用一个开源组件库,其导出了一个复杂的数据类型,如下:

export interface IData = {
    id: string;
    user: {
        name: string;
        info : {
            avatar: string;
        }
    }
}

如果我们需要预先定义一个变量去接收这个复杂类型中的「user」属性,那这个「user」变量类型该如何定义?

有一个比较low的方法去做,使用「typeof」如下:

import { data } from 'xxx';

let val: typeof data.user; // 使用typeof获取data.user的类型

...

val = data.user; // 接收data子属性user值

最为标准的做法「像使用对象属性一样去定义类型」,代码演示如下:

import { data, IData } from 'xxx';

let val: IData['user']; // 像使用对象属性一样去定义类型

...

val = data.user; // 接收data子属性user值

4. in keyof 有什么用?

「in keyof」用法较多,但最为常用的是「使用它对类型进行二次包装」。例如有如下的表单类型:

type IForm = {
    name: string;
    age: number;
    avatar: string;
}

我要将所有子属性类型包裹一层「value」,如 name: { value: string; },这时候就可以使用「in keyof」对其进行包装:

type IFormValue = {
    [K in keyof IForm]: {
        value: IForm[K];
    }
}

配合「获取子属性类型」我们还可以将「IFormValue」类型还原为「IForm」类型,如下:

type IForm = {
    [K in keyof IFormValue]: IFormValue[K]['value'];
}

「in keyof」的能力远不止于此,其他用法读者请自行探索哈~

5. 使用 extends 限定泛型类型

「泛型」的强大毋庸置疑,让下面的「函数」得以复用,并且能够得到「准确类型」的返回值:

function fn<T>(val: T) {
  console.log(`Hello ${val}`);
  return val;
}

fn(1996); // 'Hello 1996'
fn("Pwcong"); // 'Hello Pwcong'
fn({
  toString: function () {
    return "ByteDance"; 
  },
}); // 'Hello ByteDance'

我们可以传入「number」、「string」甚至一些重写了「toString」实现的「对象」类型,但是一旦我们传入了「期望外」的类型,那这个函数就会打印出奇奇怪怪的日志了。

这时候对于使用「泛型」的函数更好的方式是去使用「extends」对传入的类型进行限定,如下:

function fn<T extends number | string>(val: T) {
  console.log(`Hello ${val}`);
  return val;
}

fn(1996); // 编译通过
fn("pwcong"); // 编译通过
fn({
  toString: function () {
    return "ByteDance";
  },
}); // 类型检查报错

6. 常见的工具类型

⚠️注意:文章到这里开始简单演示常见的「工具类型」,完整的「工具类型」文档请参考官方文档地址:www.typescriptlang.org/docs/handbo…

6.1 Partial: 选填的子属性

// 「IForm」类型所有属性「必填」
type IForm = {
    name: string;
    age: number;
    avatar: string;
}

type INewForm = {
    name?: string;
    age?: number;
    avatar?: string;
}

// 这个类型跟上面的类型相同,使用「Partial」将其所有属性变为「选填」
type INewForm = Partial<IForm>;

6.2 Record: 自定义对象类型

type IForm = { [key: string]: string | number }

// 这个类型跟上面的类型相同,使用「Record」自定义对象类型
type INewForm = Record<string, string | number>;

const form: INewForm = {
    name: 'Pwcong',
    age: 18,
    avatar: 'https://xxx',
}; // 编译通过

6.3 Pick: 选取类型子属性

type IForm = {
    name: string;
    age: number;
    avatar: string;
}

type INewForm = {
    name?: string;
    avatar?: string;
}

// 这个类型跟上面的类型相同,使用「Pick」选取类型子属性
type INewForm = Pick<IForm, 'name' | 'avatar'>;

6.4 Omit: 排除类型子属性

type IForm = {
    name: string;
    age: number;
    avatar: string;
}

type INewForm = {
    name?: string;
}

// 这个类型跟上面的类型相同,使用「Omit」排除类型子属性
type INewForm = Omit<IForm, 'age' | 'avatar'>;

6.5 Exclude: 排除联合类型

type IType = number | string | boolean

type INewType = number

// 这个类型跟上面的类型相同,使用「Omit」排除类型子属性
type INewType = Exclude<IType, string | boolean>

6.6 Parameters: 获取函数形参的类型

type IFn = (name: string, age: number) => boolean;

type IName = Parameters<IFn>[0]; // 「string」类型
type IAge = Parameters<IFn>[1];// 「number」类型

6.7 ReturnType: 获取函数返回值的类型

type IFn = () => string;

type IRet = ReturnType<IFn>; // 「string」类型

总结

到这里大概过了一些「常见的TypeScript姿势」,希望能够帮助到读者们哈,如有出错还望大佬们指出改正~

转载自:https://juejin.cn/post/7064076065446035487
评论
请登录