[译]<<Effective TypeScript>> 技巧31 尽量不要null值和非null混用

217 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第23天,点击查看活动详情

本文的翻译于<<Effective TypeScript>>, 特别感谢!! ps: 本文会用简洁, 易懂的语言描述原书的所有要点. 如果能看懂这文章,将节省许多阅读时间. 如果看不懂,务必给我留言, 我回去修改.

技巧31:尽量不要null值和非null混用

null值和非null混用会有很多麻烦,完全null值或者完全非null值都比较好处理。

例如:你想写一个extent函数计算一个number数组的最大值和最小值:

function extent(nums: number[]) {
  let min, max;
  for (const num of nums) {
    if (!min) {
      min = num;
      max = num;
    } else {
      min = Math.min(min, num);
      max = Math.max(max, num);
}
  }
  return [min, max];
}

在没有strictNullChecks下,这段代码都能通过检查,并且返回值类型为 number[]。但是这段代码有bug和设计缺陷:

  • 如果min或者max值为0,那么就会出现错误,例如extent([0,1,2]) 会返回[1,2],而不是[0,2]
  • 如果nums为[], 将会返回[undefined,undefined],这种将undefined / null 值混用的行为,会给后面使用这个函数的人带来很多麻烦

strictNullChecks 打开,将会报错:

function extent(nums: number[]) {
  let min, max;
  for (const num of nums) {
    if (!min) {
      min = num;
      max = num;
    } else {
      min = Math.min(min, num);
      max = Math.max(max, num);
                  // ~~~ Argument of type 'number | undefined' is not
                  //     assignable to parameter of type 'number'
    }
  }
  return [min, max];
}

这人代码设计缺陷暴露的更明显。当你调用该函数,会出现如下问题:

const [min, max] = extent([0, 1, 2]);
const span = max - min;
          // ~~~   ~~~ Object is possibly 'undefined'

extent函数中报错的原因:你通过判断保证min不为undefined,但是没有对max进行undefined检查。、

一个更好的做法:

function extent(nums: number[]) {
  let result: [number, number] | null = null;
  for (const num of nums) {
    if (!result) {
      result = [num, num];
    } else {
      result = [Math.min(num, result[0]), Math.max(num, result[1])];
    }
  }
  return result;
}

返回值类型为:[number, number] | null,调用者将非常方便去处理:

  1. 用非空断言:
    const [min, max] = extent([0, 1, 2])!;
    const span = max - min;  // OK
    
  2. 用一个检查:
    const range = extent([0, 1, 2]);
    if (range) {
      const [min, max] = range;
      const span = max - min;  // OK
    }
    

null值非null值混用同样会在classes中导致问题的发生:

lass UserPosts {
  user: UserInfo | null;
  posts: Post[] | null;

  constructor() {
    this.user = null;
    this.posts = null;
  }

  async init(userId: string) {
    return Promise.all([
      async () => this.user = await fetchUser(userId),
      async () => this.posts = await fetchPostsForUser(userId)
    ]);
  }

  getUserName() {
    // ...?
  }
}

当在请求接口的时候,user和posts将会为null。有时,两者都可能为null,也有可能其中一个为null,也有可能两个都为非null。这让使用这个class的人非常痛苦。

更好的做法:当在请求接口的时候,用 await等待,直到所有的值都可用:

class UserPosts {
  user: UserInfo;
  posts: Post[];

  constructor(user: UserInfo, posts: Post[]) {
    this.user = user;
    this.posts = posts;
  }

  static async init(userId: string): Promise<UserPosts> {
    const [user, posts] = await Promise.all([
      fetchUser(userId),
      fetchPostsForUser(userId)
    ]);
    return new UserPosts(user, posts);
  }

  getUserName() {
    return this.user.name;
  }
}

现在 UserPosts类就是完全的非空,非常好使用。当然你非要在数据加载过程中,强行执行某些操作,判断非空也很方便。