深入解析TypeScripe几个特点 - Alex


TypeScript是一种功能强大的静态类型化语言。很多时候,它被称为“ JavaScript的超集”。但是,对于某些功能,它会强制以特定方式编写代码。
 
类魔法
TypeScript对class关键字有特殊的支持。对于(模块的)全局范围内的每个类,它隐式定义一个具有相同名称的实例类型。这样可以编写类似const user: User = new User()。
不幸的是,该机制不适用于动态创建的类或普通构造函数。在这种情况下,必须使用实用程序InstanceType和关键字typeof。

//正常类
class StaticClass {}
const a: StaticClass
/* 类型实例 */ = new StaticClass(); /* 构造器 */

//下面是
动态创建的类
const createClass = () => class {};
const DynamicClass = createClass(); /* 无隐性类型定义 */
// 现在这种写法无效: const b: DynamicClass = new DynamicClass();

type DynamicClass = InstanceType<typeof DynamicClass>;
/* 现在有了类型 */
const b: DynamicClass
/* 类型实例 */ = new DynamicClass(); /* 构造器*/

export {StaticClass, DynamicClass};
/* 都输出构造器和类型 */

语句type X = InstanceType<typeof X>在逻辑上等效于TypeScript在遇到class关键字时自动执行的操作。
 
没有成员的类型推断
对于接口的某些实现,可以推断成员属性和成员函数的类型。例如,当接口Logger定义函数log(message: string): void时,其实现类型ConsoleLogger只可以使用方法签名log(message)。TypeScript可以推断出function参数是一个字符串,返回值是void。由于不同的原因,目前不支持此功能。必须明确地显式类型化所有成员属性和成员函数,而与接口或基类无关。
下一个示例说明了由于这种情况导致的潜在重复:

interface Logger {
  logInfo(message: String): void;
  logWarning(message: String): void;
  logError(message: String): void;
}

class ConsoleLogger implements Logger {
  logInfo(message: String) { /* .. */ }
  logWarning(message: String) {
/* .. */ }
  logError(message: String) {
/* .. */ }
}

 
没有部分类型推断
TypeScript可以根据其用法来推断类型参数的类型。例如,
asArray<T>(item: T) { return [item]; }

可以在不指定类型参数(例如)的情况下调用该函数asArray('foo')。在这种情况下,T被推断为类型"foo"(extends string)。但是,这不适用于多个类型的参数,只能推断其中的一些。一种可能的解决方法是将一个函数拆分为多个,其中一个具有要推断的所有类型参数。
以下代码显示了一个通用函数,用于使用预填充的数据创建对象工厂:

const createFactory1 = <R extends {}, P extends {}>(prefilled: P) =>
  (required: R) => ({...required, ...prefilled});
// requires to specify second type parameter, even though it could be inferred
const createAdmin1 = createFactory1<{email: string}, {admin: true}>({admin: true});
const adminUser1 = createAdmin1({email: 'john@example.com'});

const createFactory2 = <R extends {}>() => <P extends {}>(prefilled: P) =>
  (required: R) => ({...required, ...prefilled});
// first function specifies type parameter, for second function it is inferred
const createAdmin2 = createFactory2<{email: string}>()({admin: true});
const adminUser2 = createAdmin2({email: 'jane@example.com'});

函数createFactory1()需要指定两个类型参数,即使可以推断出第二个参数。createFactory2()通过将该功能分为两个单独的操作,消除了此问题。
 
区分联合Discriminating Unions用法
区分联合对于处理类似项目的异类集(例如“领域事件”)很有用。该机制允许使用区分字段来区分多种类型。每种项目类型都为该字段使用一种特定的类型,以使其与众不同。处理具有联合类型的项目时,可以根据区分字段来缩小其类型。这种机制的一个缺点是,它要求以特定的方式编写代码。
下一个示例将事件处理程序的JavaScript实现与其具有Discriminate Unions的TypeScript比较:

// JavaScript
const handleEvent = ({type, data}) => {
// early destructuring
  if (type == 'UserRegistered')
    console.log(`new user with username: ${data.username}`);
  if (type == 'UserLoggedIn')
    console.log(`user logged in from device: ${data.device}`);
};

// TypeScript
type UserRegisteredEvent = {type: 'UserRegistered', data: {username: string}};
type UserLoggedInEvent = {type: 'UserLoggedIn', data: {device: string}};
type UserEvent = UserRegisteredEvent | UserLoggedInEvent;

const handleEvent = (event: UserEvent) => {
// destructuring must not happen here
  if (event.type == 'UserRegistered')
    console.log(`new user with username: ${event.data.username}`);
  if (event.type == 'UserLoggedIn')
    console.log(`user logged in from device: ${event.data.device}`);
};

使用TypeScript时,在下溯其类型之前,请勿将具有Discriminate Union类型的值进行分解。
 

模板文字Template Literal类型
模板文字类型本质上是类型级别上的模板文字。它们可用于创建字符串文字类型,这些类型是评估模板文字的结果。David Timms的文章“在TypeScript 4.1中探索模板文字类型”通过高级示例对它们进行了更详细的说明。一种值得注意的用例是消息处理组件的定义,其中各个消息类型由特定操作处理。
以下示例使用先前的记录器示例对此进行了演示:

type MessageType = 'Info' | 'Warning' | 'Error';

type Logger = {
  [k in MessageType as `log${MessageType}`]: (message: string) => void;
}

class ConsoleLogger implements Logger {
  logInfo(message: String) { /* .. */ }
  logWarning(message: String) {
/* .. */ }
  logError(message: String) {
/* .. */ }
}

类型定义Logger在联合类型MessageType上进行迭代,并为每种MessageType定义一个操作。
 
总结
尽管TypeScript的好处可能胜过其潜在的弊端,但要意识到这些弊端仍然很重要:首先,区分联盟会影响使用解构分配的方式。同样,缺少部分类型推断可能需要将一个功能拆分为多个功能。