TypeScript学习(流动的类型、混合、ThisType)

流动的类型

关键的动机:当你改变了其中一个时,其他相关的会自动更新,并且当有事情变糟糕时,你会得到一个友好的提示,就好像一个被精心设计过的约束系统。

复制类型和值

这会得到一个错误,因为 const 仅仅是复制了 Foo 到一个变量声明空间,因此你无法把 Bar 当作一个类型声明使用。正确的方式是使用 import 关键字,请注意,如果你在使用 namespace 或者 modules,使用 import 是你唯一能用的方式:

class Foo {
    
    }

const Bar = Foo;

let bar: Bar; // Error: 不能找到名称 'Bar'
namespace importing {
    
    
  export class Foo {
    
    }
}

import Bar = importing.Foo;
let bar: Bar; // ok

这个 import 技巧,仅适合于类型和变量。

捕获变量的类型

你可以通过 typeof 操作符在类型注解中使用变量。这允许你告诉编译器,一个变量的类型与其他类型相同,如下所示:

let foo = 123;
let bar: typeof foo; // 'bar' 类型与 'foo' 类型相同(在这里是: 'number')

bar = 456; // ok
bar = '789'; // Error: 'string' 不能分配给 'number' 类型

捕获类成员的类型

class Foo {
    
    
  foo: number; // 我们想要捕获的类型
}

declare let _foo: Foo;

// 与之前做法相同
let bar: typeof _foo.foo;

捕获字符串类型

// 捕获字符串的类型与值
const foo = 'Hello World';

// 使用一个捕获的类型
let bar: typeof foo;

// bar 仅能被赋值 'Hello World'
bar = 'Hello World'; // ok
bar = 'anything else'; // Error

捕获键的名称

keyof 操作符能让你捕获一个类型的键。例如,你可以使用它来捕获变量的键名称,在通过使用 typeof 来获取类型之后:

const colors = {
    
    
  red: 'red',
  blue: 'blue'
};

type Colors = keyof typeof colors;

let color: Colors; // color 的类型是 'red' | 'blue'
color = 'red'; // ok
color = 'blue'; // ok
color = 'anythingElse'; // Error

这允许你很容易地拥有像字符串枚举+常量这样的类型,如上例所示。

异常处理

JavaScript 有一个 Error 类,用于处理异常。你可以通过 throw 关键字来抛出一个错误。然后通过 try/catch 块来捕获此错误:

try {
    
    
  throw new Error('Something bad happened');
} catch (e) {
    
    
  console.log(e);
}

使用Error

JavaScript 初学者可能有时候仅仅是抛出一个原始字符串:

try {
    
    
  throw 'Something bad happened';
} catch (e) {
    
    
  console.log(e);
}

不要这么做,使用 Error 对象的基本好处是,它能自动跟踪堆栈的属性构建以及生成位置。原始字符串会导致极差的调试体验,并且在分析日志时,将会变得错综复杂

你并不需要 throw 抛出一个错误

传递一个 Error 对象是没问题的,这种方式在 Node.js 回调函数中非常常见,它用第一个参数作为错误对象进行回调处理。

function myFunction (callback: (e: Error)) {
    
    
  doSomethingAsync(function () {
    
    
    if (somethingWrong) {
    
    
      callback(new Error('This is my error'));
    } else {
    
    
      callback();
    }
  })
}

正确使用方法

「Exceptions should be exceptional」是计算机科学中常用用语。这里有一些原因说明在 JavaScript(TypeScript) 中也是如此。

  • 不清楚从哪里抛出错误
try {
    
    
  const foo = runTask1();
  const bar = runTask2();
} catch (e) {
    
    
  console.log('Error:', e);
}

下一个开发者可能并不清楚哪个函数可能会抛出错误。在没有阅读 task1/task2 代码以及他们可能会调用的函数时,对代码 review 的人员可能也不会知道错误会从哪里抛出。

  • 优雅的捕获错误
try {
    
    
  const foo = runTask1();
} catch (e) {
    
    
  console.log('Error:', e);
}

try {
    
    
  const bar = runTask2();
} catch (e) {
    
    
  console.log('Error:', e);
}

但是现在,如果你想从第一个任务中传递变量到第二个任务中,代码会变的混乱(注意:foo 变量需要用 let 显式注解它,因为它不能从 runTask1 中返回出来):

let foo: number; // Notice 使用 let 并且显式注明类型注解

try {
    
    
  foo = runTask1();
} catch (e) {
    
    
  console.log('Error:', e);
}

try {
    
    
  const bar = runTask2(foo);
} catch (e) {
    
    
  console.log('Error:', e);
}

没有在类型系统中很好的表示

function validate(value: number) {
    
    
  if (value < 0 || value > 100) {
    
    
    throw new Error('Invalid value');
  }
}

在这种情境下使用 Error 不是一个好的主意。因为没有用来验证函数的类型定义(如:(value: number) => void),取而代之一个更好的方式是创建一个验证方法:

function validate(
  value: number
): {
    
    
  error?: string;
} {
    
    
  if (value < 0 || value > 100) {
    
    
    return {
    
     error: 'Invalid value' };
  }
}

除非你想用以非常通用(try/catch)的方式处理错误,否则不要抛出错误。

混合

TypeScript (和 JavaScript) 类只能严格的单继承,因此你不能做:

class User extends Tagged, Timestamped {
    
     // ERROR : 不能多重继承
  // ..
}

从可重用组件构建类的另一种方式是通过基类来构建它们,这种方式称为混合。

这个主意是简单的,采用函数 B 接受一个类 A,并且返回一个带有新功能的类的方式来替代 A 类扩展 B 来获取 B 上的功能,前者中的 B 即是混合。

一个完整的例子:

// 所有 mixins 都需要
type Constructor<T = {
     
     }> = new (...args: any[]) => T;

/
// mixins 例子


// 添加属性的混合例子
function TimesTamped<TBase extends Constructor>(Base: TBase) {
    
    
  return class extends Base {
    
    
    timestamp = Date.now();
  };
}

// 添加属性和方法的混合例子
function Activatable<TBase extends Constructor>(Base: TBase) {
    
    
  return class extends Base {
    
    
    isActivated = false;

    activate() {
    
    
      this.isActivated = true;
    }

    deactivate() {
    
    
      this.isActivated = false;
    }
  };
}

///
// 组合类
///

// 简单的类
class User {
    
    
  name = '';
}

// 添加 TimesTamped 的 User
const TimestampedUser = TimesTamped(User);

// Tina TimesTamped 和 Activatable 的类
const TimestampedActivatableUser = TimesTamped(Activatable(User));

//
// 使用组合类
//

const timestampedUserExample = new TimestampedUser();
console.log(timestampedUserExample.timestamp);

const timestampedActivatableUserExample = new TimestampedActivatableUser();
console.log(timestampedActivatableUserExample.timestamp);
console.log(timestampedActivatableUserExample.isActivated);

创建一个构造函数

混合接受一个类,并且使用新功能扩展它。因此,我们需要定义构造函数的类型:

type Constructor<T = {
     
     }> = new (...args: any[]) => T;

扩展一个类并且返回它

// 添加属性的混合例子
function TimesTamped<TBase extends Constructor>(Base: TBase) {
    
    
  return class extends Base {
    
    
    timestamp = Date.now();
  };
}

ThisType

通过 ThisType 我们可以在对象字面量中键入 this,并提供通过上下文类型控制 this 类型的便捷方式。它只有在 --noImplicitThis 的选项下才有效。

现在,在对象字面量方法中的 this 类型,将由以下决定:

  • 如果这个方法显式指定了 this 参数,那么 this 具有该参数的类型。(下例子中 bar)
  • 否则,如果方法由带 this 参数的签名进行上下文键入,那么 this 具有该参数的类型。(下例子中 foo)
  • 否则,如果 --noImplicitThis 选项已经启用,并且对象字面量中包含由 ThisType 键入的上下文类型,那么 this 的类型为 T。
  • 否则,如果 --noImplicitThis 选项已经启用,并且对象字面量中不包含由 ThisType 键入的上下文类型,那么 this 的类型为该上下文类型。
  • 否则,如果 --noImplicitThis 选项已经启用,this 具有该对象字面量的类型。
  • 否则,this 的类型为 any。
// Compile with --noImplicitThis
type Point = {
    
    
  x: number;
  y: number;
  moveBy(dx: number, dy: number): void;
};

let p: Point = {
    
    
  x: 10,
  y: 20,
  moveBy(dx, dy) {
    
    
    this.x += dx; // this has type Point
    this.y += dy; // this has type Point
  }
};

let foo = {
    
    
  x: 'hello',
  f(n: number) {
    
    
    this; // { x: string, f(n: number): void }
  }
};

let bar = {
    
    
  x: 'hello',
  f(this: {
    
     message: string }) {
    
    
    this; // { message: string }
  }
};

类似的方式,当使用 --noImplicitThis 时,函数表达式赋值给 obj.xxx 或者 obj[xxx] 的目标时,在函数中 this 的类型将会是 obj:

// Compile with --noImplicitThis

obj.f = function(n) {
    
    
  return this.x - n; // 'this' has same type as 'obj'
};

obj['f'] = function(n) {
    
    
  return this.x - n; // 'this' has same type as 'obj'
};

通过 API 转换参数的形式来生成 this 的值的情景下,可以通过创建一个新的 ThisType 标记接口,可用于在上下文中表明转换后的类型。尤其是当字面量中的上下文类型为 ThisType 或者是包含 ThisType 的交集时,显得尤为有效,对象字面量方法中 this 的类型即为 T。

type ObjectDescriptor<D, M> = {
    
    
  data?: D;
  methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M
};

function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
    
    
  let data: object = desc.data || {
    
    };
  let methods: object = desc.methods || {
    
    };
  return {
    
     ...data, ...methods } as D & M;
}

let obj = makeObject({
    
    
  data: {
    
     x: 0, y: 0 },
  methods: {
    
    
    moveBy(dx: number, dy: number) {
    
    
      this.x += dx; // Strongly typed this
      this.y += dy; // Strongly typed this
    }
  }
});

obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);

在上面的例子中,makeObject 参数中的对象属性 methods 具有包含 ThisType<D & M> 的上下文类型,因此对象中 methods 属性下的方法的 this 类型为 { x: number, y: number } & { moveBy(dx: number, dy: number): number }。

ThisType 的接口,在 lib.d.ts 只是被声明为空的接口,除了可以在对象字面量上下文中可以被识别以外,该接口的作用等同于任意空接口。

详解案例:

// 没有ThisType情况下
const foo = {
    
    
    bar() {
    
    
         console.log(this.a); // error,在foo中只有bar一个函数,不存在a
    }
}

// 使用ThisType
const foo: {
    
     bar: any } & ThisType<{
    
     a: number }> = {
    
    
    bar() {
    
    
         console.log(this.bar) // error,因为没有在ThisType中定义
         console.log(this.a); // ok
    }
}

foo.bar // ok
foo.a // error,在外面的话,就跟ThisType没有关系了

Guess you like

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