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>{}