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的好处可能胜过其潜在的弊端,但要意识到这些弊端仍然很重要:首先,区分联盟会影响使用解构分配的方式。同样,缺少部分类型推断可能需要将一个功能拆分为多个功能。