复杂场景下的 typescript 类型锚定 (2) ----- 概念回炉

avatar
前端开发工程师 @阿里巴巴

作者:ICBU 东墨

写在最前:欢迎来到阿里巴巴 ICBU 交互&端技术前端团队专栏,我们将与你分享优质的前端技术文章,欢迎关注&交流哟!

继上次分享了 typescript 类型锚定的方法后,和同事们又进行了一些点对点的讨论。在交流的过程中,我发现部分同学已经能较为熟练地使用 ts 的类型推导,但对某些不常见的 typescript 关键字还比较陌生。

为了在理解更复杂的类型锚定场景,我们先重温一些基础知识,巩固印象,以便更清晰地理解 typescript 的常用概念

保留字 / Reserve Keyword

import

import 是 es module 标准,在 typescript 中,import 也用于类型的导入,关于 import 所有的 es module 写法在对于类型空间都是有效的:

import defaultExport from "module-name";

// import 属于 es module,它兼容对 commonjs 模块的引入,如下一行
import * as name from "module-name";

// 具名模块
import { export1 } from "module-name";

// 同时使用 default 和具名模块
import defaultExport, { export1 [ , [...] ] } from "module-name";

除此外,看到下面的用法也不必惊讶:

import mysql = require('@aliexample/mysql')
// equivelant to 
import * mysql from '@aliexample/mysql'

export

export 是 es module 标准,在 typescript 中,export 也用于类型的导出,关于 export 所有的 es module 写法在对于类型空间都是有效的:

// 导出多个类型
export { name1, name2, …, nameN };

// 别名
export { variable1 as name1, variable2 as name2, …, nameN };

// 默认模块
export default variable1;

除此外,看到下面的用法也不必惊讶:

// 导出整个 mod,这意味着对该上下文(比如一个 npm 包的入口),它提供了一个 commonjs 风格的模块
export = mod;

type/interface/class/namespace/module/function

这些关键字都可以用来定义类型:

type A = string
type B = number
type C = symbol
type D = {
    foo: 'bar'
}
// equivelant to
interface D = {
    foo: 'bar'
}
type F1 = () => void
type F2 = (input: string) => string
// union F1 and F2
type F3 = F1 | F2
// but you can also write like this
type F3 = {
    (): void
    (input: string): string
}

// symbol
// this is real symbol
type symbol_instance = symbol
// this is contructor!
type SymbolItSelf = Symbol

// all part of enums is one SOLID string, but enums doesn't equal to string!
type enums = 'a' | 'b' | 'c'

declare namepsace Scope {
     // write any type definition here...
     type A = string
     type B = number
     // ...
     class Query {
       constructor (a: string)
     }
}

// use/refernce A from Scope
type ARef = Scope.A
// use/refernce B from Scope
type BRef = Scope.B

关于命名空间的使用,之后我们再单独讨论其内部成员的导出、可访问性和隐蔽性

类型分析

我们再来做一些类型分析.

构造函数参数提取

这是 typescript 3.5 后引入的内置类型,它的含义是“求一个函数的所有参数”. 关于 extends 的含义, 请参考这篇文章强大的 extends 三元推导 一节.

 type Parameters<T> = T extends (...args: infer T) => any ? T : never;

这里一个问题是, Parameters 是否可以用来求某个类的构造函数的所有参数? 比如:

declare namespace A {
    class Query {
        constructor (a: string)
    }

    // is that ok?
    type ConstructorParamsOfQuery = Parameters<Query['constructor']>
}

实际上是不行的, 因为 Query['constructor'] 的类型是 (Function )并不符合 Parameters<T> 中的 extends 规则

Query['constructor']: Function

注意这里, 一般 我们无法给在 typescript 类型定义中给类的 constructor 指明返回类型(虽然实际上 javascript 允许你在 construtor 中返回和其宿主类完全不同的变量), 所以也不用想着去 class Query 上修改 constructor.

这时候我们就需要一个新的推导类型, 专门处理类 constructor 的参数:

type ConstructorParams<T> = T extends {
    new (...args: infer U): any
} ? U : never

declare namespace A {
    class Query {
        constructor (a: string)
    }

    // that's ok!
    type ConstructorParamsOfQuery = ConstructorParams<typeof Query>
}

现在你可以看到, A.ConstructorParamsOfQuery 等价于 [string], 即 A.Query 构造函数的参数类型列表.

注意 在上述定义中, ConstructorParams 会假设其泛型 T 是一个类函数对象, 这时候要把 typeof Query 作为 T 传给 ConstructorParams(而不是直接传 Query)

小结

最近我和涉及 Wap 搜索/PC Detail/天马搭建/Lighthub 等场景的同学的交流中,发现了一些常见知识盲区和疑难点,对此我给出了建议以解决这些问题。

此过程证明了 typescript 各业务线间在做业务对接时的一些积极效果:

  • typescript 类型能使得各自提供的 npm 包具有更好的可读性
  • typescript 类型会引导模块用户(尤其是 npm 包用户)能按照模块提供方的设计意图正确使用, 降低沟通成本, 降低错误率, 提高了业务安全性

但 typescript 不是万金油, 并非所有的代码安全问题能靠 typescript 解决. 至于开发者要如何在开发中实践业务保障的安全意识, 那又是另一个话题了.

❤️ 感谢你看到最后~

阿里巴巴国际站(ICBU,Alibaba.com)是全球最大的跨境贸易和服务平台。我们时刻有新的技术挑战,有足够有趣的挑战满足你所有的好奇心和求知欲,有国外知名合作团队(Google & OpenSky)。

如果你想来 ICBU 和我一起开发前端,欢迎发简历到邮箱 shudai.lyy@alibaba-inc.com ,我们将快速响应你的面试安排。:-)