TypeScript 类型缩小

TypeScript 变量的值可以变,但是类型通常是不变的。唯一允许的改变,就是类型缩小,就是将变量值的范围缩得更小。

手动类型缩小

如果一个变量属于联合类型,所以使用时一般需要缩小类型。

第一种方法是使用if判断。

  1. function getScore(value: number|string): number {
  2. if (typeof value === 'number') { // (A)
  3. // %inferred-type: number
  4. value;
  5. return value;
  6. }
  7. if (typeof value === 'string') { // (B)
  8. // %inferred-type: string
  9. value;
  10. return value.length;
  11. }
  12. throw new Error('Unsupported value: ' + value);
  13. }

如果一个值是anyunknown,你又想对它进行处理,就必须先缩小类型。

  1. function parseStringLiteral(stringLiteral: string): string {
  2. const result: unknown = JSON.parse(stringLiteral);
  3. if (typeof result === 'string') { // (A)
  4. return result;
  5. }
  6. throw new Error('Not a string literal: ' + stringLiteral);
  7. }

下面是另一个例子。

  1. interface Book {
  2. title: null | string;
  3. isbn: string;
  4. }
  5. function getTitle(book: Book) {
  6. if (book.title === null) {
  7. // %inferred-type: null
  8. book.title;
  9. return '(Untitled)';
  10. } else {
  11. // %inferred-type: string
  12. book.title;
  13. return book.title;
  14. }
  15. }

缩小类型的前提是,需要先获取类型。获取类型的几种方法如下。

  1. function func(value: Function|Date|number[]) {
  2. if (typeof value === 'function') {
  3. // %inferred-type: Function
  4. value;
  5. }
  6. if (value instanceof Date) {
  7. // %inferred-type: Date
  8. value;
  9. }
  10. if (Array.isArray(value)) {
  11. // %inferred-type: number[]
  12. value;
  13. }
  14. }

typeof 运算符

第二种方法是使用switch缩小类型。

  1. function getScore(value: number|string): number {
  2. switch (typeof value) {
  3. case 'number':
  4. // %inferred-type: number
  5. value;
  6. return value;
  7. case 'string':
  8. // %inferred-type: string
  9. value;
  10. return value.length;
  11. default:
  12. throw new Error('Unsupported value: ' + value);
  13. }
  14. }

instanceof 运算符

第三种方法是instanceof运算符。它能够检测实例对象与构造函数之间的关系。instanceof运算符的左操作数为实例对象,右操作数为构造函数,若构造函数的prototype属性值存在于实例对象的原型链上,则返回true;否则,返回false。

  1. function f(x: Date | RegExp) {
  2. if (x instanceof Date) {
  3. x; // Date
  4. }
  5. if (x instanceof RegExp) {
  6. x; // RegExp
  7. }
  8. }

instanceof类型守卫同样适用于自定义构造函数,并对其实例对象进行类型细化。

  1. class A {}
  2. class B {}
  3. function f(x: A | B) {
  4. if (x instanceof A) {
  5. x; // A
  6. }
  7. if (x instanceof B) {
  8. x; // B
  9. }
  10. }

in 运算符

第四种方法是使用in运算符。

in运算符是JavaScript中的关系运算符之一,用来判断对象自身或其原型链中是否存在给定的属性,若存在则返回true,否则返回false。in运算符有两个操作数,左操作数为待测试的属性名,右操作数为测试对象。

in类型守卫根据in运算符的测试结果,将右操作数的类型细化为具体的对象类型。

  1. interface A {
  2. x: number;
  3. }
  4. interface B {
  5. y: string;
  6. }
  7. function f(x: A | B) {
  8. if ('x' in x) {
  9. x; // A
  10. } else {
  11. x; // B
  12. }
  13. }
  1. interface A { a: number }
  2. interface B { b: number }
  3. function pickAB(ab: A | B) {
  4. if ('a' in ab) {
  5. ab // Type is A
  6. } else {
  7. ab // Type is B
  8. }
  9. ab // Type is A | B
  10. }

缩小对象的属性,要用in运算符。

  1. type FirstOrSecond =
  2. | {first: string}
  3. | {second: string};
  4. function func(firstOrSecond: FirstOrSecond) {
  5. if ('second' in firstOrSecond) {
  6. // %inferred-type: { second: string; }
  7. firstOrSecond;
  8. }
  9. }
  10. // 错误
  11. function func(firstOrSecond: FirstOrSecond) {
  12. // @ts-expect-error: Property 'second' does not exist on
  13. // type 'FirstOrSecond'. [...]
  14. if (firstOrSecond.second !== undefined) {
  15. // ···
  16. }
  17. }

in运算符只能用于联合类型,不能用于检查一个属性是否存在。

  1. function func(obj: object) {
  2. if ('name' in obj) {
  3. // %inferred-type: object
  4. obj;
  5. // 报错
  6. obj.name;
  7. }
  8. }

特征属性

对于不同对象之间的区分,还可以人为地为每一类对象设置一个特征属性。

  1. interface UploadEvent {
  2. type: 'upload';
  3. filename: string;
  4. contents: string
  5. }
  6. interface DownloadEvent { type: 'download'; filename: string; }
  7. type AppEvent = UploadEvent | DownloadEvent;
  8. function handleEvent(e: AppEvent) {
  9. switch (e.type) {
  10. case 'download':
  11. e // Type is DownloadEvent
  12. break;
  13. case 'upload':
  14. e; // Type is UploadEvent
  15. break;
  16. }
  17. }

any 类型的细化

TypeScript 推断变量类型时,会根据获知的信息,不断改变推断出来的类型,越来越细化。这种现象在any身上特别明显。

  1. function range(
  2. start:number,
  3. limit:number
  4. ) {
  5. const out = []; // 类型为 any[]
  6. for (let i = start; i < limit; i++) {
  7. out.push(i);
  8. }
  9. return out; // 类型为 number[]
  10. }

上面示例中,变量out的类型一开始推断为any[],后来在里面放入数值,类型就变为number[]

再看下面的例子。

  1. const result = []; // 类型为 any[]
  2. result.push('a');
  3. result // 类型为 string[]
  4. result.push(1);
  5. result // 类型为 (string | number)[]

上面示例中,数组result随着成员类型的不同,而不断改变自己的类型。

注意,这种any类型的细化,只在打开了编译选项noImplicitAny时发生。

这时,如果在变量的推断类型还为any时(即没有任何写操作),就去输出(或读取)该变量,则会报错,因为这时推断还没有完成,无法满足noImplicitAny的要求。

  1. const result = []; // 类型为 any[]
  2. console.log(typeof result); // 报错
  3. result.push('a'); // 类型为 string[]

上面示例中,只有运行完第三行,result的类型才能完成第一次推断,所以第二行读取result就会报错。

is 运算符

is运算符返回一个布尔值,用来判断左侧的值是否属于右侧的类型。

  1. function isInputElement(el: HTMLElement): el is HTMLInputElement {
  2. return 'value' in el;
  3. }
  4. function getElementContent(el: HTMLElement) {
  5. if (isInputElement(el)) {
  6. el; // Type is HTMLInputElement
  7. return el.value;
  8. }
  9. el; // Type is HTMLElement
  10. return el.textContent;
  11. }
  1. function isDefined<T>(x: T | undefined): x is T {
  2. return x !== undefined;
  3. }