Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript: Deserialize from JSON to Discriminated Union

Given the Typescript code snippet:

class Excel {
    Password: string;
    Sheet: number;
}

class Csv {
    Separator: string;
    Encoding: string;
}

type FileType = Excel | Csv

let input = '{"Separator": ",", "Encoding": "UTF-8"}';

let output = Object.setPrototypeOf(JSON.parse(input), FileType.prototype)   // error!

In TypeScript/Javascript, to deserialize from JSON, one can use Object.setPrototypeOf(), the second parameter of which requires a "prototype". With classes, e.g. Excel, one can just do Excel.prototype. But with Discriminated Union as above, I encountered an error:

error TS2693: 'FileType' only refers to a type, but is being used as a value here.

Question:

  1. Is there any way to deserialize a Discriminated Union in TypeScript?
  2. If not, is there any other elegant way to realize the scenario above (given two classes: Excel/Csv and JSON string serializing either of them; get back the correct instantiated object), regardless of whatever tricks, classes, class inheritance, interface, discriminated union or not...?

Environment

  • Typescript v2.9.2
  • Visual Studio Code v1.25.1

My try

let json = JSON.parse(input);
let output: FileType | null = null;
if (json["Separator"]) {
    console.log("It's csv");
    output = Object.setPrototypeOf(json, Csv.prototype)
} else if (json["Password"]) {
    console.log("It's excel");
    output = Object.setPrototypeOf(json, Excel.prototype)
} else {
    console.log("Error");
}

It's easy to recognize that this approach is cumbersome (if else alot), especially when adding new classes. Additionally, developers have to choose a unique field for checking in each class...

like image 943
JoyfulPanda Avatar asked Nov 08 '22 04:11

JoyfulPanda


1 Answers

In your example, FileType is not a class, it is only a compile-time union type. No runtime code is generated for FileType, so while the type checker understands what it means, there is no FileType object defined at runtime to retrieve the protoptype property from.

I'm not clear why you need to set the prototype of the deserialized object in the first place. Why not just declare it this way:

let output = JSON.parse(input) as FileType;
if (IsExcel(output)) { /* do stuff with output.Password & .Sheet */ }
else { /* do stuff with output.Seperator and .Encoding }

IsExcel() is to determine if the deserialized object is of the Excel type, it should be written as a User-Defined Type Guard, but it might be something like

function IsExcel(f: FileType): f is Excel { . . . } 

IsExcel returns a boolean, but by writing the return type this way TypeScript understands it is reading the discriminator for the discriminated union. You can check any way you want, for example by checking if (<any>f).Sheet is defined.

like image 98
Burt_Harris Avatar answered Nov 15 '22 11:11

Burt_Harris