辨析联合类型

当类中含有字面量成员时,我们可以用该类的属性来辨析联合类型。

做为一个例子,考虑 SquareRectangle 的联合类型 ShapeSquareRectangle有共同成员 kind,因此 kind 存在于 Shape 中。

  1. interface Square {
  2. kind: 'square';
  3. size: number;
  4. }
  5. interface Rectangle {
  6. kind: 'rectangle';
  7. width: number;
  8. height: number;
  9. }
  10. type Shape = Square | Rectangle;

如果你使用类型保护风格的检查(=====!=!==)或者使用具有判断性的属性(在这里是 kind),TypeScript 将会认为你会使用的对象类型一定是拥有特殊字面量的,并且它会为你自动把类型范围变小:

  1. function area(s: Shape) {
  2. if (s.kind === 'square') {
  3. // 现在 TypeScript 知道 s 的类型是 Square
  4. // 所以你现在能安全使用它
  5. return s.size * s.size;
  6. } else {
  7. // 不是一个 square ?因此 TypeScript 将会推算出 s 一定是 Rectangle
  8. return s.width * s.height;
  9. }
  10. }

详细的检查

通常,联合类型的成员有一些自己的行为(代码):

  1. interface Square {
  2. kind: 'square';
  3. size: number;
  4. }
  5. interface Rectangle {
  6. kind: 'rectangle';
  7. width: number;
  8. height: number;
  9. }
  10. // 有人仅仅是添加了 `Circle` 类型
  11. // 我们可能希望 TypeScript 能在任何被需要的地方抛出错误
  12. interface Circle {
  13. kind: 'circle';
  14. radius: number;
  15. }
  16. type Shape = Square | Rectangle | Circle;

一个可能会让你的代码变差的例子:

  1. function area(s: Shape) {
  2. if (s.kind === 'square') {
  3. return s.size * s.size;
  4. } else if (s.kind === 'rectangle') {
  5. return s.width * s.height;
  6. }
  7. // 如果你能让 TypeScript 给你一个错误,这是不是很棒?
  8. }

你可以通过一个简单的向下思想,来确保块中的类型被推断为与 never 类型兼容的类型。例如,你可以添加一个更详细的检查来捕获错误:

  1. function area(s: Shape) {
  2. if (s.kind === 'square') {
  3. return s.size * s.size;
  4. } else if (s.kind === 'rectangle') {
  5. return s.width * s.height;
  6. } else {
  7. // Error: 'Circle' 不能被赋值给 'never'
  8. const _exhaustiveCheck: never = s;
  9. }
  10. }

它将强制你添加一种新的条件:

  1. function area(s: Shape) {
  2. if (s.kind === 'square') {
  3. return s.size * s.size;
  4. } else if (s.kind === 'rectangle') {
  5. return s.width * s.height;
  6. } else if (s.kind === 'circle') {
  7. return Math.PI * s.radius ** 2;
  8. } else {
  9. // ok
  10. const _exhaustiveCheck: never = s;
  11. }
  12. }

Switch

TIP

你可以通过 switch 来实现以上例子。

  1. function area(s: Shape) {
  2. switch (s.kind) {
  3. case 'square':
  4. return s.size * s.size;
  5. case 'rectangle':
  6. return s.width * s.height;
  7. case 'circle':
  8. return Math.PI * s.radius ** 2;
  9. default:
  10. const _exhaustiveCheck: never = s;
  11. }
  12. }

strictNullChecks

如果你使用 strictNullChecks 选项来做详细的检查,你应该返回 _exhaustiveCheck 变量(类型是 never),否则 TypeScript 可能会推断返回值为 undefined

  1. function area(s: Shape) {
  2. switch (s.kind) {
  3. case 'square':
  4. return s.size * s.size;
  5. case 'rectangle':
  6. return s.width * s.height;
  7. case 'circle':
  8. return Math.PI * s.radius ** 2;
  9. default:
  10. const _exhaustiveCheck: never = s;
  11. return _exhaustiveCheck;
  12. }
  13. }

Redux

Redux 库正是使用的上述例子。

以下是添加了 TypeScript 类型注解的redux 要点

  1. import { createStore } from 'redux';
  2. type Action =
  3. | {
  4. type: 'INCREMENT';
  5. }
  6. | {
  7. type: 'DECREMENT';
  8. };
  9. /**
  10. * This is a reducer, a pure function with (state, action) => state signature.
  11. * It describes how an action transforms the state into the next state.
  12. *
  13. * The shape of the state is up to you: it can be a primitive, an array, an object,
  14. * or even an Immutable.js data structure. The only important part is that you should
  15. * not mutate the state object, but return a new object if the state changes.
  16. *
  17. * In this example, we use a `switch` statement and strings, but you can use a helper that
  18. * follows a different convention (such as function maps) if it makes sense for your
  19. * project.
  20. */
  21. function counter(state = 0, action: Action) {
  22. switch (action.type) {
  23. case 'INCREMENT':
  24. return state + 1;
  25. case 'DECREMENT':
  26. return state - 1;
  27. default:
  28. return state;
  29. }
  30. }
  31. // Create a Redux store holding the state of your app.
  32. // Its API is { subscribe, dispatch, getState }.
  33. let store = createStore(counter);
  34. // You can use subscribe() to update the UI in response to state changes.
  35. // Normally you'd use a view binding library (e.g. React Redux) rather than subscribe() directly.
  36. // However it can also be handy to persist the current state in the localStorage.
  37. store.subscribe(() => console.log(store.getState()));
  38. // The only way to mutate the internal state is to dispatch an action.
  39. // The actions can be serialized, logged or stored and later replayed.
  40. store.dispatch({ type: 'INCREMENT' });
  41. // 1
  42. store.dispatch({ type: 'INCREMENT' });
  43. // 2
  44. store.dispatch({ type: 'DECREMENT' });
  45. // 1

与 TypeScript 一起使用可以有效的防止拼写错误,并且能提高重构和书写文档化代码的能力。

原文: https://jkchao.github.io/typescript-book-chinese/typings/discrominatedUnion.html