TypeScript学习(Never、辨析联合类型、索引签名)

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
};

Guess you like

Origin blog.csdn.net/shadowfall/article/details/121325235