代码之家  ›  专栏  ›  技术社区  ›  Jan Molak michaeltwofish

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

  •  1
  • Jan Molak michaeltwofish  · 技术社区  · 6 年前

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

    我的 current implementation 使用 type-level programming 它可以与typescript 3.0一起工作,但感觉它可以简化,以充分利用TS 3.3中的新语言功能,而且没有黑客的味道。

    力学

    假设我们已经有一个名为 TinyType 用一种方法 toJSON .

    该方法序列化 钛型 根据以下规则,我希望 系列化<t> 要表示的接口,以便子类可以定义为:

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

    示例1-基元值包装器

    当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 回复  |  直到 6 年前
        1
  •  2
  •   Titian Cernicova-Dragomir    6 年前

    这应该按预期工作:

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

    只是一些警告,没有广泛的测试,所以你可能会 any 在某种程度上,如果编译器认为类型太复杂。 Id 只是为了美观的原因才把它弄平,如果你遇到问题,就用它吧 Id<T> = T 看看塔特能不能修好。

    如果您有任何问题,请告诉我,我会尽力回答,解决方案基本上只是一个映射和条件类型的直接应用程序,就像您认为的那样。