Never
程序语言的设计确实应该存在一个底部类型的概念,当你在分析代码流的时候,这会是一个理所当然存在的类型。TypeScript 就是这样一种分析代码流的语言,因此它需要一个可靠的,代表永远不会发生的类型。 never 类型是 TypeScript 中的底层类型。它自然被分配的一些例子:
一个从来不会有返回值的函数(如:如果函数内含有 while(true) {}); 一个总是会抛出错误的函数(如:function foo() { throw new Error(‘Not Implemented’) },foo 的返回类型是 never);
你也可以将它用做类型注解:但是,never 类型仅能被赋值给另外一个 never:
let foo: never = 123;
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;
}
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') {
return s.size * s.size;
} else {
return s.width * s.height;
}
}
详细的检查
interface Square {
kind: 'square';
size: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
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;
}
}
你可以通过一个简单的向下思想,来确保块中的类型被推断为与 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 {
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 {
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';
};
function counter(state = 0, action: Action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
let store = createStore(counter);
store.subscribe(() => console.log(store.getState()));
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
索引签名
可以用字符串访问 JavaScript 中的对象(TypeScript 中也一样),用来保存对其他对象的引用。
let foo: any = {};
foo['Hello'] = 'World';
console.log(foo['Hello']);
我们在键 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();
当你传入一个其他对象至索引签名时,JavaScript 会在得到结果之前会先调用 .toString 方法:
let obj = {
toString() {
console.log('toString called');
return 'Hello';
}
};
let foo: any = {};
foo[obj] = 'World';
console.log(foo[obj]);
console.log(foo['Hello']);
只要索引位置使用了 obj,toString 方法都将会被调用。
数组有点稍微不同,对于一个 number 类型的索引签名,JavaScript 引擎将会尝试去优化(这取决于它是否是一个真的数组、存储的项目结构是否匹配等)。因此,number 应该被考虑作为一个有效的对象访问器(这与 string 不同),如下例子:
let foo = ['World'];
console.log(foo[0]);
TypeScript 索引签名
const obj = {
toString() {
return 'Hello';
}
};
const foo: any = {};
foo[obj] = 'World';
foo[obj.toString()] = 'World';
强制用户必须明确的写出 toString() 的原因是:在对象上默认执行的 toString 方法是有害的。例如 v8 引擎上总是会返回 [object Object]
const obj = { message: 'Hello' };
let foo: any = {};
foo[obj] = 'World';
console.log(foo['[object Object]']);
当然,数字类型是被允许的,这是因为:
需要对数组 / 元组完美的支持; 即使你在上例中使用 number 类型的值来替代 obj,number 类型默认的 toString 方法实现的很友好(不是 [object Object])。
因此,我们有以下结论:
TypeScript 的索引签名必须是 string 或者 number。symbols 也是有效的,TypeScript 支持它。
声明一个索引签名
你也可以指定索引签名
const foo: {
[index: string]: { message: string };
} = {};
foo['a'] = { message: 'some message' };
foo['a'] = { messages: 'some message' };
foo['a'].message;
foo['a'].messages;
所有成员都必须符合字符串的索引签名
interface Foo {
[key: string]: number;
x: number;
y: number;
}
interface Bar {
[key: string]: number;
x: number;
y: string;
}
这可以给你提供安全性,任何以字符串的访问都能得到相同结果。
interface Foo {
[key: string]: number;
x: number;
}
let foo: Foo = {
x: 1,
y: 2
};
foo['x'];
const x = 'x';
foo[x];
使用一组有限的字符串字面量
type Index = 'a' | 'b' | 'c';
type FromIndex = { [k in Index]?: number };
const good: FromIndex = { b: 1, c: 2 };
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;
length: number;
}
设计模式:索引签名的嵌套
interface NestedCSS {
color?: string;
[selector: string]: string | NestedCSS;
}
const example: NestedCSS = {
color: 'red',
'.subclass': {
color: 'blue'
}
};
尽量不要使用这种把字符串索引签名与有效变量混合使用。如果属性名称中有拼写错误,这个错误不会被捕获到:
const failsSilently: NestedCSS = {
colour: 'red'
};
取而代之,我们把索引签名分离到自己的属性里,如命名为 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'
}
索引签名中排除某些属性
有时,你需要把属性合并至索引签名(虽然我们并不建议这么做,你应该使用上文中提到的嵌套索引签名的形式),如下例子:
type FieldState = {
value: string;
};
type FromState = {
isValid: boolean;
[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 };
declare const foo: FormState;
const isValidBool = foo.isValid;
const somethingFieldState = foo['something'];
const bar: FormState = {
isValid: false
};
|