将类递归映射到其JSON表示形式的类型安全方法

时间:2019-02-09 22:16:14

标签: typescript generics

我正在尝试实现一个名为Serialised<T>的接口,以表示给定类的公共非功能属性的递归序列化。

我的current implementation使用type-level programming并可以使用TypeScript 3.0,但是感觉可以简化它以利用TS 3.3中的新语言功能,并且不会有黑客的味道。 / p>

机械师

假设我们已经有一个名为TinyType的基类,并且具有方法toJSON

该方法根据以下规则对TinyType的任何子类进行序列化,我希望Serialised<T>接口表示该子类,以便可以将子类定义为:

class MyClass extends TinyType {
    toJSON(): Serialised<MyClass> {...}
}

示例1-原始值包装器

当TinyType包装单个值时,它将序列化为该单个值将被序列化为的值。

对于基元,TinyType的序列化表示就是基元本身:

class Name extends TinyType {
    constructor(public readonly name: string) {}
}

new Name('Bob').toJSON() === 'Bob';   // toJSON(): string


class Age extends TinyType {
    constructor(public readonly age: string) {}
}

new Age(42).toJSON() === 42 .         // toJSON(): number

示例2-TinyType包装器

但是,我们也可能遇到单值TinyType包装另一个单值TinyType的情况,在这种情况下,第一个示例中的规则将递归地应用:

class Integer extends TinyType {
    constructor(public readonly value: number) {
      // some logic that ensures that value is an integer...
    }
}

class AmountInCents extends TinyType {
    constructor(public readonly amountInCents: Integer) {}
}

class Credit extends TinyType {
    constructor(public readonly amount: AmountInCents) {}
}

new Credit(new AmountInCents(new Integer(100))).toJSON() === 100
// toJSON(): number

示例3:多值包装器

当TinyType包装多个值时,应将其序列化为JSON对象,并使用键表示TinyType的公共非功能属性,并使用值表示其序列化版本。

class Timestamp extends TinyType {
    constructor(public readonly value: Integer) {}
}

class Credit extends TinyType {
    constructor(public readonly amount: AmountInCents) {}
}

class CreditRecorded extends TinyType {
    constructor(
        public readonly credit: Credit,
        public readonly timestamp: Timestamp,
    ) {}        
}

new CreditRecorded(
  new Credit(new AmountInCents(new Integer(100))),
  new Timestamp(new Integer(1234567)),
).toJSON() === { credit: 100, timestamp: 1234567 }

到目前为止,我的研究表明该解决方案可以利用:

我当然可以将toJSON()定义为返回JSONValue,并避免将类映射到其序列化表示形式的麻烦,但是感觉可以在这里做得更好?

欢迎提出建议!

1 个答案:

答案 0 :(得分:2)

这应该可以正常工作:

type NotFunctions<T, E extends keyof T> = {
    [P in Exclude<keyof T, E>]-?: T[P] extends Function ? never : P
}[Exclude<keyof T, E>]

type UnionToIntersection<U> = 
    (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never

type Unwrap<T> = T extends { toJSON(): infer U } ? U : T;
type PickAndUnwrap<T, K extends keyof T> = {
    [P in K] : Unwrap<T[P]>
}

type SimpleOrComplex<T, E extends keyof T> =  NotFunctions<T, E> extends UnionToIntersection<NotFunctions<T, E>>?
    PickAndUnwrap<T, NotFunctions<T, E>>[NotFunctions<T, E>] :
    PickAndUnwrap<T, NotFunctions<T, E>>

type Id<T> = T extends object ? {} & { [P in keyof T] : T[P]} : T

class TinyType {
    public toJSON(): Id<SimpleOrComplex< this, keyof TinyType>> {
        return null!;
    }
}

class Name extends TinyType {
    constructor(public readonly name: string) {
        super();
    }
}

new Name('Bob').toJSON() === "" // toJSON(): string


class Age extends TinyType {
    constructor(public readonly age: number) {
        super()
    }
}

new Age(42).toJSON() === 42          // toJSON(): number

class Integer extends TinyType {
    constructor(public readonly value: number) {
        super();
    }
}

class AmountInCents extends TinyType {
    constructor(public readonly amountInCents: Integer) {
        super();
    }
}

class Credit extends TinyType {
    constructor(public readonly amount: AmountInCents) {
        super()
    }
}
new AmountInCents(new Integer(100)).toJSON
new Credit(new AmountInCents(new Integer(100))).toJSON() === 100
// toJSON(): number

class Timestamp extends TinyType {
    constructor(public readonly value: Integer) {
        super();
    }
}


class CreditRecorded extends TinyType {
    constructor(
        public readonly credit: Credit,
        public readonly timestamp: Timestamp,
    ) {
        super();
    }        
}

new CreditRecorded(
  new Credit(new AmountInCents(new Integer(100))),
  new Timestamp(new Integer(1234567)),
).toJSON() === { credit: 100, timestamp: 1234567 }

class Person extends TinyType {
    constructor(
        public readonly name: Name,
        public readonly creditRecord: CreditRecorded,
        public readonly age: Integer) {
        super();
    }
}

new Person(new Name(""), new CreditRecorded(
  new Credit(new AmountInCents(new Integer(100))),
  new Timestamp(new Integer(1234567)),
), new Integer(23)).toJSON() // { readonly name: string; readonly creditRecord: { readonly credit: number; readonly timestamp: number; }; readonly age: number; }

仅需注意一些事项,并未进行广泛的测试,因此,如果编译器认为类型太复杂,您可能会更深入地了解anyId只是出于装饰性原因而扁平化类型,如果遇到问题,只需使用Id<T> = T并查看taht是否可以解决该问题。

如果您有任何问题让我知道,我会尽力回答,解决方案通常只是像您想的那样直接应用映射类型和条件类型。