Appearance
辨析联合类型
当类中含有字面量成员时,我们可以用该类的属性来辨析联合类型。
作为一个例子,考虑 Square
和 Rectangle
的联合类型 Shape
。Square
和 Rectangle
有共同成员 kind
,因此 kind
存在于 Shape
中。
ts
interface Square {
kind: 'square';
size: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
type Shape = Square | Rectangle;
如果你使用类型保护风格的检查(==
、===
、!=
、!==
)或者使用具有判断性的属性(在这里是 kind
),TypeScript 将会认为你会使用的对象类型一定是拥有特殊字面量的,并且它会为你自动把类型范围变小:
ts
function area(s: Shape) {
if (s.kind === 'square') {
// 现在 TypeScript 知道 s 的类型是 Square
// 所以你现在能安全使用它
return s.size * s.size;
} else {
// 不是一个 square ?因此 TypeScript 将会推算出 s 一定是 Rectangle
return s.width * s.height;
}
}
详细的检查
通常,联合类型的成员有一些自己的行为(代码):
ts
interface Square {
kind: 'square';
size: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
// 有人仅仅是添加了 `Circle` 类型
// 我们可能希望 TypeScript 能在任何被需要的地方抛出错误
interface Circle {
kind: 'circle';
radius: number;
}
type Shape = Square | Rectangle | Circle;
一个可能会让你的代码变差的例子:
ts
function area(s: Shape) {
if (s.kind === 'square') {
return s.size * s.size;
} else if (s.kind === 'rectangle') {
return s.width * s.height;
}
// 如果你能让 TypeScript 给你一个错误,这是不是很棒?
}
你可以通过一个简单的向下思想,来确保块中的类型被推断为与 never
类型兼容的类型。例如,你可以添加一个更详细的检查来捕获错误:
ts
function area(s: Shape) {
if (s.kind === 'square') {
return s.size * s.size;
} else if (s.kind === 'rectangle') {
return s.width * s.height;
} else {
// Error: 'Circle' 不能被赋值给 'never'
const _exhaustiveCheck: never = s;
}
}
它将强制你添加一种新的条件:
ts
function area(s: Shape) {
if (s.kind === 'square') {
return s.size * s.size;
} else if (s.kind === 'rectangle') {
return s.width * s.height;
} else if (s.kind === 'circle') {
return Math.PI * s.radius ** 2;
} else {
// ok
const _exhaustiveCheck: never = s;
}
}
Switch
TIP
你可以通过 switch
来实现以上例子。
ts
function area(s: Shape) {
switch (s.kind) {
case 'square':
return s.size * s.size;
case 'rectangle':
return s.width * s.height;
case 'circle':
return Math.PI * s.radius ** 2;
default:
const _exhaustiveCheck: never = s;
}
}
strictNullChecks
如果你使用 strictNullChecks
选项来做详细的检查,你应该返回 _exhaustiveCheck
变量(类型是 never
),否则 TypeScript 可能会推断返回值为 undefined
:
ts
function area(s: Shape) {
switch (s.kind) {
case 'square':
return s.size * s.size;
case 'rectangle':
return s.width * s.height;
case 'circle':
return Math.PI * s.radius ** 2;
default:
const _exhaustiveCheck: never = s;
return _exhaustiveCheck;
}
}
Redux
Redux 库正是使用的上述例子。
以下是添加了 TypeScript 类型注解的redux 要点。
ts
import { createStore } from 'redux';
type Action =
| {
type: 'INCREMENT';
}
| {
type: 'DECREMENT';
};
/**
* This is a reducer, a pure function with (state, action) => state signature.
* It describes how an action transforms the state into the next state.
*
* The shape of the state is up to you: it can be a primitive, an array, an object,
* or even an Immutable.js data structure. The only important part is that you should
* not mutate the state object, but return a new object if the state changes.
*
* In this example, we use a `switch` statement and strings, but you can use a helper that
* follows a different convention (such as function maps) if it makes sense for your
* project.
*/
function counter(state = 0, action: Action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
// Create a Redux store holding the state of your app.
// Its API is { subscribe, dispatch, getState }.
let store = createStore(counter);
// You can use subscribe() to update the UI in response to state changes.
// Normally you'd use a view binding library (e.g. React Redux) rather than subscribe() directly.
// However it can also be handy to persist the current state in the localStorage.
store.subscribe(() => console.log(store.getState()));
// The only way to mutate the internal state is to dispatch an action.
// The actions can be serialized, logged or stored and later replayed.
store.dispatch({ type: 'INCREMENT' });
// 1
store.dispatch({ type: 'INCREMENT' });
// 2
store.dispatch({ type: 'DECREMENT' });
// 1
与 TypeScript 一起使用可以有效的防止拼写错误,并且能提高重构和书写文档化代码的能力。