Never
程序语言的设计确实应该存在一个底部类型的概念,当你在分析代码流的时候,这会是一个理所当然存在的类型。TypeScript 就是这样一种分析代码流的语言,因此它需要一个可靠的,代表永远不会发生的类型。
never 类型是 TypeScript 中的底层类型。它自然被分配的一些例子:
一个从来不会有返回值的函数(如:如果函数内含有 while(true) {});
一个总是会抛出错误的函数(如:function foo() { throw new Error(‘Not Implemented’) },foo 的返回类型是 never);
你也可以将它用做类型注解:但是,never 类型仅能被赋值给另外一个 never:
let foo: never = 123; // Error: number 类型不能赋值给 never 类型
// ok, 作为函数返回类型的 never
let bar: never = (() => {
throw new Error('Throw my hands in the air like I just dont care');
})();
用例:详细的检查
function foo(x: string | number): boolean {
if (typeof x === 'string') {
return true;
} else if (typeof x === 'number') {
return false;
}
// 如果不是一个 never 类型,这会报错:
// - 不是所有条件都有返回值 (严格模式下)
// - 或者检查到无法访问的代码
// 但是由于 TypeScript 理解 `fail` 函数返回为 `never` 类型
// 它可以让你调用它,因为你可能会在运行时用它来做安全或者详细的检查。
return fail('Unexhaustive');
}
function fail(message: string): never {
throw new Error(message);
}
与 void 的差异(void表示无返回值类型、Never代表无返回的值)
一旦有人告诉你,never 表示一个从来不会优雅的返回的函数时,你可能马上就会想到与此类似的 void,然而实际上,void 表示没有任何类型,never 表示永远不存在的值的类型。
当一个函数返回空值时,它的返回值为 void 类型,但是,当一个函数永不返回时(或者总是抛出错误),它的返回值为 never 类型。void 类型可以被赋值(在 strictNullChecking 为 false 时),但是除了 never 本身以外,其他任何类型不能赋值给 never。
辨析联合类型
当类中含有字面量成员时,我们可以用该类的属性来辨析联合类型。
作为一个例子,考虑 Square 和 Rectangle 的联合类型 Shape。Square 和 Rectangle有共同成员 kind,因此 kind 存在于 Shape 中。
interface Square {
kind: 'square';
size: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
type Shape = Square | Rectangle;
如果你使用检查(== 、 === 、!=、!==)或者使用具有判断性的属性(在这里是 kind),TypeScript 将会认为你会使用的对象类型一定是拥有特殊字面量的,并且它会为你自动把类型范围变小:
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;
}
}
详细的检查
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;
一个可能会让你的代码变差的例子:
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 类型兼容的类型。例如,你可以添加一个更详细的检查来捕获错误:
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;
}
}
它将强制你添加一种新的条件:
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
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:
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
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
索引签名
可以用字符串访问 JavaScript 中的对象(TypeScript 中也一样),用来保存对其他对象的引用。
let foo: any = {};
foo['Hello'] = 'World';
console.log(foo['Hello']); // World
我们在键 Hello 下保存了一个字符串 World,除字符串外,它也可以保存任意的 JavaScript 对象,例如一个类的实例。
class Foo {
constructor(public message: string) {}
log() {
console.log(this.message);
}
}
let foo: any = {};
foo['Hello'] = new Foo('World');
foo['Hello'].log(); // World
当你传入一个其他对象至索引签名时,JavaScript 会在得到结果之前会先调用 .toString 方法:
let obj = {
toString() {
console.log('toString called');
return 'Hello';
}
};
let foo: any = {};
foo[obj] = 'World'; // toString called
console.log(foo[obj]); // toString called, World
console.log(foo['Hello']); // World
只要索引位置使用了 obj,toString 方法都将会被调用。
数组有点稍微不同,对于一个 number 类型的索引签名,JavaScript 引擎将会尝试去优化(这取决于它是否是一个真的数组、存储的项目结构是否匹配等)。因此,number 应该被考虑作为一个有效的对象访问器(这与 string 不同),如下例子:
let foo = ['World'];
console.log(foo[0]); // World
TypeScript 索引签名
const obj = {
toString() {
return 'Hello';
}
};
const foo: any = {};
// ERROR: 索引签名必须为 string, number....
foo[obj] = 'World';
// FIX: TypeScript 强制你必须明确这么做:
foo[obj.toString()] = 'World';
强制用户必须明确的写出 toString() 的原因是:在对象上默认执行的 toString 方法是有害的。例如 v8 引擎上总是会返回 [object Object]
const obj = { message: 'Hello' };
let foo: any = {};
// ERROR: 索引签名必须为 string, number....
foo[obj] = 'World';
// 这里实际上就是你存储的地方
console.log(foo['[object Object]']); // World
当然,数字类型是被允许的,这是因为:
需要对数组 / 元组完美的支持;
即使你在上例中使用 number 类型的值来替代 obj,number 类型默认的 toString 方法实现的很友好(不是 [object Object])。
因此,我们有以下结论:
TypeScript 的索引签名必须是 string 或者 number。symbols 也是有效的,TypeScript 支持它。
声明一个索引签名
你也可以指定索引签名
const foo: {
[index: string]: { message: string }; // 这里的index没有任何意义,你也可以写username 来让你的代码更容易理解
} = {};
// 储存的东西必须符合结构
// ok
foo['a'] = { message: 'some message' };
// Error, 必须包含 `message`
foo['a'] = { messages: 'some message' };
// 读取时,也会有类型检查
// ok
foo['a'].message;
// Error: messages 不存在
foo['a'].messages;
所有成员都必须符合字符串的索引签名
// ok
interface Foo {
[key: string]: number;
x: number;
y: number;
}
// Error
interface Bar {
[key: string]: number;
x: number;
y: string; // Error: y 属性必须为 number 类型
}
这可以给你提供安全性,任何以字符串的访问都能得到相同结果。
interface Foo {
[key: string]: number;
x: number;
}
let foo: Foo = {
x: 1,
y: 2
};
// 直接
foo['x']; // number
// 间接
const x = 'x';
foo[x]; // number
使用一组有限的字符串字面量
type Index = 'a' | 'b' | 'c';
type FromIndex = { [k in Index]?: number };
const good: FromIndex = { b: 1, c: 2 };
// Error:
// `{ b: 1, c: 2, d: 3 }` 不能分配给 'FromIndex'
// 对象字面量只能指定已知类型,'d' 不存在 'FromIndex' 类型上
const bad: FromIndex = { b: 1, c: 2, d: 3 };
变量的规则一般可以延迟被推断:
type FromSomeIndex<K extends string> = { [key in K]: number };
同时拥有 string 和 number 类型的索引签名
这并不是一个常见的用例,但是 TypeScript 支持它。string 类型的索引签名比 number 类型的索引签名更严格。这是故意设计,它允许你有如下类型:
interface ArrStr {
[key: string]: string | number; // 必须包括所用成员类型
[index: number]: string; // 字符串索引类型的子级
// example
length: number;
}
设计模式:索引签名的嵌套
interface NestedCSS {
color?: string; // strictNullChecks=false 时索引签名可为 undefined
[selector: string]: string | NestedCSS;
}
const example: NestedCSS = {
color: 'red',
'.subclass': {
color: 'blue'
}
};
尽量不要使用这种把字符串索引签名与有效变量混合使用。如果属性名称中有拼写错误,这个错误不会被捕获到:
const failsSilently: NestedCSS = {
colour: 'red' // 'colour' 不会被捕捉到错误
};
取而代之,我们把索引签名分离到自己的属性里,如命名为 nest(或者 children、subnodes 等):
interface NestedCSS {
color?: string;
nest?: {
[selector: string]: NestedCSS;
};
}
const example: NestedCSS = {
color: 'red',
nest: {
'.subclass': {
color: 'blue'
}
}
}
const failsSliently: NestedCSS = {
colour: 'red' // TS Error: 未知属性 'colour'
}
索引签名中排除某些属性
有时,你需要把属性合并至索引签名(虽然我们并不建议这么做,你应该使用上文中提到的嵌套索引签名的形式),如下例子:
type FieldState = {
value: string;
};
type FromState = {
isValid: boolean; // Error: 不符合索引签名
[filedName: string]: FieldState;
};
TypeScript 会报错,因为添加的索引签名,并不兼容它原有的类型,使用交叉类型可以解决上述问题:
type FieldState = {
value: string;
};
type FormState = { isValid: boolean } & { [fieldName: string]: FieldState };
请注意尽管你可以声明它至一个已存在的 TypeScript 类型上,但是你不能创建如下的对象:
type FieldState = {
value: string;
};
type FormState = { isValid: boolean } & { [fieldName: string]: FieldState };
// 将它用于从某些地方获取的 JavaScript 对象 declare可以向TypeScript域中引入一个变量,在编写代码的时候就能够实现智能提示的功能。
declare const foo: FormState;
const isValidBool = foo.isValid;
const somethingFieldState = foo['something'];
// 使用它来创建一个对象时,将不会工作
const bar: FormState = {
// 'isValid' 不能赋值给 'FieldState'
isValid: false
};