现代编程语言:TypeScript
0x01 安装
- 自行Google
在线执行测试|playground
https://www.typescriptlang.org/play
00x2 语言内核
- 字符串常量类型
- 接口(interface)
- 类型别名(Type)
- 泛型(Generic)
- 泛型约束
- 泛型构造器
- 方法继承与覆盖
- UnionType+IntersectionType
- 枚举
0x03 实战案例
TypeScript的泛型约束
TypeScript的泛型存在和C#的泛型一样的使用上的不便利,泛型参数在作用域内能够调用的方法一定是要通过泛型参数的约束来指定的,例如一个泛型函数:
function test<T>(){ let t = new T(); // 构造实例,compiler error! let x = t.method(); // 调用成员方法,compiler error! let x = t.static_method(); // 调用静态方法,compiler error! }
无论是调用方法还是构造T的实例,都不能直接通过,需要给T添加约束来解决对应的问题:
interface Something{ method():number; } interface SomethingBuilder<T>{ new(...constructorArgs: any[]): T; static_method():number; } // 1. 约束T扩展了Something,从而t可以调用Something的方法。 // 2. 约束了C扩展了构造T的匿名接口 // 从而constructor: C可以被用来构造T的实例t // 3. 添加一个新的泛型C,C扩展了SomethingBuilder<T> // 这个构造Something的元接口, // 同时把构造需要的类型和参数作为test函数的参数传入 function test< T extends Something, C extends SomethingBuilder<T> >(builder: C, ...args: any[]){ let t = new builder(args); // OK let x = t.method(); // OK let x = builder.static_method(); // OK } // 用例: class Some implements Something{ constructor(){ // } static static_method():number{ return 0; } method():number{ return 1; } } test(Some);
Union类型的模式匹配(1)
TypeScript的Union类型并没有语言原生支持的类型模式匹配语法,下面这种enum+子类化+union类型定义
的方式可以作为一种在TypeScript里对Union类型模式匹配的实践方案,不过对于没有继承关系的一组类型组成的Union类型,就不能达到方便的使用switch去“模式匹配”了:
export enum SignDataType{ Rsa1024 = 0, Rsa2048 = 1, Ecc = 2 }; export abstract class SignDataBase{ type: SignDataType; // 添加公共的类型tag constructor(type: SignDataType){ this.type = type; } } export class Rsa1024SignData extends SignDataBase{ constructor(){ super(SignDataType.Rsa1024); } } export class Rsa2048SignData extends SignDataBase{ constructor(){ super(SignDataType.Rsa2048); } } export class EccSignData extends SignDataBase{ constructor(){ super(SignDataType.Ecc); } } export type SignData = (Rsa1024SignData|Rsa2048SignData|EccSignData) & SignDataBase; function test(t: SignData){ // 使用普通的swich-case对类型tag分支 switch(t.type){ case SignDataType.Rsa2048:{ // }, case SignDataType.Rsa1024:{ // }, case SignDataType.Ecc:{ // } } }
Union类型的模式匹配(2)
当然,我们可以结合通常的Visitor模式
和lambda函数
来实现一个方法级别的模式匹配:
export interface ObjectIdInfoPartten<T>{ StandardObjectIdInfo:(info:StandardObjectIdInfo)=>T; CoreObjectIdInfo:(info:CoreObjectIdInfo)=>T; DecAppObjectIdInfo:(info:DecAppObjectIdInfo)=>T; } export interface ObjectIdInfoMatcher{ match<T>(p: ObjectIdInfoPartten<T>):T; } export class StandardObjectIdInfo implements ObjectIdInfoMatcher{ obj_type_code: ObjectTypeCode; obj_type: number; area: Option<Area>; constructor(obj_type_code: ObjectTypeCode, obj_type: number, area: Option<Area>){ this.obj_type_code = obj_type_code; this.obj_type = obj_type; this.area = area; } match<T>(p: ObjectIdInfoPartten<T>):T{ return p.StandardObjectIdInfo(this); } } export class CoreObjectIdInfo implements ObjectIdInfoMatcher{ area: Option<Area>; has_owner: boolean; has_single_key: boolean; has_mn_key: boolean; constructor(area: Option<Area>, has_owner: boolean, has_single_key: boolean, has_mn_key: boolean){ this.area = area; this.has_owner = has_owner; this.has_single_key = has_single_key; this.has_mn_key = has_mn_key; } match<T>(p: ObjectIdInfoPartten<T>):T{ return p.CoreObjectIdInfo(this); } } export class DecAppObjectIdInfo implements ObjectIdInfoMatcher{ area: Option<Area>; has_owner: boolean; has_single_key: boolean; has_mn_key: boolean; constructor(area: Option<Area>, has_owner: boolean, has_single_key: boolean, has_mn_key: boolean){ this.area = area; this.has_owner = has_owner; this.has_single_key = has_single_key; this.has_mn_key = has_mn_key; } match<T>(p: ObjectIdInfoPartten<T>):T{ return p.DecAppObjectIdInfo(this); } } export type ObjectIdInfo = (StandardObjectIdInfo | CoreObjectIdInfo | DecAppObjectIdInfo) & ObjectIdInfoMatcher;
测试代码如下:
function test(info: ObjectIdInfo){ // 使用方法级别的match // 这实际上是一个visitor模式,把不同类型的visitor通过lambda函数注入 info.match({ StandardObjectIdInfo:(info):void=>{ // }, CoreObjectIdInfo: (info):void=>{ // }, DecAppObjectIdInfo: (info):void=>{ // } }); }
Union类型解决了的问题是什么?
在TypeScript里写了一下午的Union类型+Visitor模式代码。我觉的这里的区别值得分析一下讲解给大家听。
-----分割线1-----
在Rust里面可以轻易的做到:
- 使用枚举类型把毫无关系的不同类型组合成一个抽象的类型,这样使用的时候就可以把它们当作一个类型,极大的增加了抽象的效率:
enum SomeType{ Name1(Type1), Name2(Type2) }
- 但是它们事实上是不同的类型,使用的时候,还需要能分离它们来用。因此Rust提供了模式匹配的能力:
fn test(a: SomeType){ match a{ Name1(t)=>{ // 处理Type1的逻辑 }, Name2(t)=>{ // 处理Type2的逻辑 } } }
-----分割线2-----
上述Rust的做法,如果没有Rust的联合类型,在普通的OOP语言里,你需要达成上述【1】【2】的目的,也就是先打包类型,到处统一处理,在局部拆包分别处理。那么,传统OOP的做法就是使用继承(无论是继承抽象类还是接口继承)。
传统OOP的做法,你可以把公共的部分实现到基类里面;不同的部分实现到子类里面。那么问题是什么呢?从这个帖子的角度来说有两点:
-
你没办法把任意不同的类型,也就是那种无论从属性还是方法上都没有任何长的像的不同类型打包成一个抽象类型去使用。但事实上我们的编程里有大量的这种需求,只是传统OOP的语法想定了你的想象。一旦开发出这种编程思维,你会发现代码在很多地方的抽象可以做大幅度的【编码压缩】。更强和自然的抽象能力,带来的是对复杂度的更大规模的控制。
-
没有模式匹配。传统OOP的不同子类,需要把父类转成子类去处理的过程并不简洁。一种做法是使用访问者(Visitor)模式去处理。但是如果没有函数式语法支持,写起来会比较啰嗦。
-----分割线3-----
现在,我们来看下TypeScript。TypeScript提供了Union Type。例如你可以定义一个联合类型:
type SomeType = Type1 | Type2;
这样无论式Type1还是Type2的实例,都可以当作SomeType使用,我们达到了本帖讨论的能力之一,也就是打包不同的类型作为一个类型抽象去使用。
但是,TypeScript受限于向下兼容JavaScript,目前编译器并不提供模式匹配的能力。你需要自己依赖具体类型的具体的字段信息做出判断,当前接收到的一个SomeType的实例,它,到底是Type1呢?还是Type2呢?
折衷的做法之一是给每个具体的类型添加一个tag,使用最普通的switch-case:
swtich(t.tag){ case "type1": .. case "type2" : . }
另外一种做法就是结合访问者模式来做. 每个具体的类型都实现一个访问者接口。
// 泛型+lambda快速定义访问者接口
interface Visitor<T>{ Name1: (t: Type1)=>T, Name2: (t: Type2)=>T, }
// 定义一个模式匹配接口
interface Matcher{ match<T>(v: Visitor<T>):T; } class Type1 implements Matcher{ match<T>(v: Visitor<T>):T{ // 选择自己的那个分支“访问” return v:Name1(this); } } class Type2 implements Matcher{ match<T>(v: Visitor<T>):T{ // 选择自己的那个分支“访问” return v:Name2(this); } }
// 用例:
function test(t: SomeType){ // 所有的t都有match方法 // 传入一个包含所有类型访问分支的访问者 t.match({ Name1:(t)=>{ // Type1 处理 }, Name2:(t)=>{ // Type2 处理 } }) }
总的来说,TypeScript提供了一个半成品,但是至少函数式+访问者模式可以折衷实现模式匹配。上述代码可以进一步简化下:
export class ViewBalanceResult { private readonly tag: number; private constructor( private single?: ViewSingleBalanceResult, private union?: ViewUnionBalanceResult, ){ if(single) { this.tag = 0; } else if(union) { this.tag = 1; } else { this.tag = -1; } } static Single(single: ViewSingleBalanceResult): ViewBalanceResult { return new ViewBalanceResult(single); } static Union(union: ViewUnionBalanceResult): ViewBalanceResult { return new ViewBalanceResult(undefined, union); } match<T>(visitor: { Single?: (single: ViewSingleBalanceResult)=>T, Union?: (union: ViewUnionBalanceResult)=>T, }):T|undefined{ switch(this.tag){ case 0: return visitor.Single?.(this.single!); case 1: return visitor.Union?.(this.union!); default: break; } } eq_type(rhs: ViewBalanceResult):boolean{ return this.tag===rhs.tag; } }
用例如下:
// 类似 Rust 子对象被枚举对象装箱一层 const u = ViewBalanceResult.Single(new ViewSingleBalanceResult()); // 模式匹配,可以只处理需要处理的分支 u.match({ Single:(s:ViewSingleBalanceResult)=>{ console.log(s); } });
泛型类type的问题
有时候我们定义了一个泛型类型,例如:
class NamedObject<K,V>{ constructor(....){...} //... }
如果K,V的名字也很长,例如DeviceDescContent
, DeviceBodyContent
, 每次使用时,都要写:NamedObject<DeviceDescContent, DeviceBodyContent>
,此时可以用type
给一个别名改进:
type Device NamedObject<DeviceDescContent, DeviceBodyContent>;
但是,type
只是一个类型别名,它又不能直接作为类型构造器使用:
let d = new Device(); // compile error!
况且,有时候我们希望给Device添加一些新的方法,此时也无法对type Device
进行扩展。因此,一种解决方式就是使用子类化的方式:
class Device extends NamedObject<DeviceDescContent, DeviceBodyContent>{ constructor(...){ // ... super(...); //注意使用this之前调用父类构造函数 } extmethod(){ } } let d = new Device(...); // compile success! d.extmethod(); // extension method call success!
通过子类化可以让泛型中的类型参数具体化,同时支持扩展新方法。在Rust里面,则不必如此,直接通过trait
这种type class
对类型进行静态扩展。
TypeScript 的 Set/Map 的问题
Set/Map 只是简单翻译成 JavaScript 的 Set/Map,因此对于非 string/symbol 做 key 的需求,由于其 key 的比较采用的是 JavaScript 的 Value 比较机制,并不满足其他语言里的 HashSet/HashMap的需求。
一个简单的做法是,封装一层:
首先,定义一个 Key 比较接口,需要做 HashSet/HashMap 的键的对象应该实现 keyCompare 接口。
export interface KeyCompare{ key(): symbol; }
其次,封装 HashSet:
export class HashSetValues<V> implements Iterable<V>{ constructor(public values: IterableIterator<V>){ // ignore } [Symbol.iterator](){ return this; } next():IteratorResult<V> { const n = this.values.next(); if(n.done){ return {value: undefined, done: true}; }else{ return {value: n.value, done: false} } } } export class HashSetEntries<V> implements Iterable<[V,V]>{ constructor(public values: IterableIterator<V>){ // ignore } [Symbol.iterator](){ return this; } next():IteratorResult<[V,V]> { const n = this.values.next(); if(n.done){ return {value: undefined, done: true}; }else{ const v = n.value; return {value: [v,v], done: false} } } } export class HashSet<T extends KeyCompare> { private readonly hash_map: Map<symbol, T>; constructor(){ this.hash_map = new Map<symbol, T>(); } get size():number { return this.hash_map.size; } add(v: T): HashSet<T>{ const k = v.key(); if(!this.hash_map.has(k)){ this.hash_map.set(k, v); } return this; } clear(): void{ this.hash_map.clear(); } delete(v: T):boolean { const k = v.key(); return this.hash_map.delete(k); } has(v: T):boolean { const k = v.key(); return this.hash_map.has(k); } keys(): IterableIterator<T> { return new HashSetValues(this.hash_map.values()); } values(): IterableIterator<T> { return new HashSetValues(this.hash_map.values()); } entries(): IterableIterator<[T, T]> { const s = new Set(); return new HashSetEntries(this.hash_map.values()); } forEach(callback: (value: T, value2: T, set: HashSet<T>)=>void){ for(const e of this.entries()){ callback(e[0], e[1], this); } } to<K1>(ke:(k:T)=>K1):Set<K1>{ const map = new Set<K1>(); for(const v of this.values()){ map.add(ke(v)); } return map; } }
最后,封装下 HashMap
export class HashMapKeys<K,V> implements Iterable<K>{ constructor(public values: IterableIterator<[K,V]>){ // ignore } [Symbol.iterator](){ return this; } next():IteratorResult<K> { const n = this.values.next(); if(n.done){ return {value: undefined, done: true}; }else{ const [k,v] = n.value; return {value: k, done: false} } } } export class HashMapValues<K,V> implements Iterable<V>{ constructor(public values: IterableIterator<[K,V]>){ // ignore } [Symbol.iterator](){ return this; } next():IteratorResult<V> { const n = this.values.next(); if(n.done){ return {value: undefined, done: true}; }else{ const [k,v] = n.value; return {value: v, done: false} } } } export class HashMapEntries<K,V> implements Iterable<[K,V]>{ constructor(public values: IterableIterator<[K,V]>){ // ignore } [Symbol.iterator](){ return this; } next():IteratorResult<[K,V]> { const n = this.values.next(); if(n.done){ return {value: undefined, done: true}; }else{ const [k,v] = n.value; return {value: [k,v], done: false} } } } export class HashMap<K extends KeyCompare, V extends RawEncode> { private hash_map: Map<symbol, [K,V]>; constructor(){ this.hash_map = new Map(); } get size(): number{ return this.hash_map.size; } clear(){ this.hash_map.clear(); } delete(key: K){ const key_s = key.key(); this.hash_map.delete(key_s); } has(key: K){ const key_s = key.key(); return this.hash_map.has(key_s); } set(key: K, v: V){ const key_s = key.key(); this.hash_map.set(key_s,[key, v]); } get(key: K): V|undefined { const key_s = key.key(); return this.hash_map.get(key_s)?.[1]; } keys(): IterableIterator<K> { return new HashMapKeys(this.hash_map.values()); } values(): IterableIterator<V> { return new HashMapValues(this.hash_map.values()); } entries(): IterableIterator<[K,V]> { return new HashMapEntries(this.hash_map.values()); } forEach(callback: (value: V, key: K, map: HashMap<K,V>)=>void){ for(const [s, [k, v]] of this.hash_map){ callback(v, k, this); } } to<K1,V1>(ke:(k:K)=>K1, ve:(v:V)=>V1):Map<K1,V1>{ const map = new Map<K1,V1>(); for(const [k,v] of this.entries()){ map.set(ke(k),ve(v)); } return map; } }
上述封装需要理解下 JavaScript 的迭代器的结构,迭代器需实现 Iterable
[Symbol.iterator](){}
next():IteratorResult<T>{}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,携手博客园推出1Panel与Halo联合会员
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次 .NET某固高运动卡测试 卡慢分析
· 微服务架构学习与思考:微服务拆分的原则
· 记一次 .NET某云HIS系统 CPU爆高分析
· 如果单表数据量大,只能考虑分库分表吗?
· 一文彻底搞懂 MCP:AI 大模型的标准化工具箱
· 博客园2025新款「AI繁忙」系列T恤上架
· Avalonia跨平台实战(二),Avalonia相比WPF的便利合集(一)
· C# LINQ 快速入门实战指南,建议收藏学习!
· Redis实现高并发场景下的计数器设计
· 上周热点回顾(4.7-4.13)