Easier way to enforce typed immutable data structures? - immutable.js

Enforcing a data structure with vanilla JS objects and flow is easy:
type ExampleObjType = {
key1: number,
key2: string
};
const obj: ExampleObjType = {
key1: 123,
key2: '123'
};
It seems like this requires an unnecessarily large amount of boilerplate to enforce a similar structure in Immutable:
type TestSpec = {
key1: number,
key2: string,
};
const TestRecord = Record({
key1: 0,
key2: '',
});
const record: TestSpec & Record<TestSpec> = new TestRecord({
key1: 123,
key2: '123',
});
Further, the above structure has some major drawbacks:
Forced default values
No enforcement on invalid keys on initialization, only on access
Ideally I would be able to use Immutable.Map, something like this:
type TestSpec = Map<{key1: number, key2: number}>;
const testMap: TestSpec = Map({
key1: 123,
key2: '123',
});
However, the current implementation only allows typing of key and value type. I could constrain the key type using something hokey like type Key = 'key1' | 'key2', but that still wouldn't me to explicitly type the values per key.
Is there a way to accomplish what I'm looking for with Immutable? It seems like a very foundational requirement to true type safety, particularly when used for, say, Redux action payloads.

immutable.Record() is difficult to type. In fact, it's currently typed to return any. On one hand, this is inconvenient, since you'll need to describe the type of TestRecord from scratch. On the other hand, it lets you type it however you like!
Here's one option:
interface TestSpec {
constructor(defaults: $Shape<TestSpec>): void,
key1: number,
key2: string,
};
const TestRecord: Class<TestSpec> = Record({
key1: 0,
key2: '',
});
The Class<TestSpec> type is the type of a class who's instances have the type TestSpec. You don't have to define the constructor in TestSpec, but by doing so, Flow will make sure obj has the right time when it sees new TestRecord(obj).
(Note: $Shape<TestSpec> is the type of an object which may or may not have. So in this case it basically means {key1?: number, key2?: string})
new TestRecord({key1: 123, key2: '123'}); // OK
new TestRecord({key1: 123}); // OK
new TestRecord({key2: '123'}); // OK
new TestRecord({key2: 123}); // Error number -> string
new TestRecord({key1: '123'}); // Error string -> number
new TestRecord({key1: 123, key2: '123', key3: 'blah'}); // Error unknown property key3
And Flow will check that instance properties have the right type
const record = new TestRecord({key1: 123, key2: '123'});
(record.key1: string); // Error number -> string
(record.key2: number); // Error string -> number
Try this code on flowtype.org/try

Related

Is it possible to use something like JsonPath with kotlin JSON parsing

I have a json structure that I need to (sort of) flatten when serializing it into an object. Some of the elements are at the top level and some are in a sub field. In addition, 1 of the fields is an array of space delimited strings that I need to parse and represent as myString.splig(" ")[0]
So, short of a when expression to do the job, can I use something like a jsonpath query to bind to certain fields? I have thought of even doing some kind of 2-pass binding and then merging both instances.
{
"key": "FA-207542",
"fields": {
"customfield_10443": {
"value": "TBD"
},
"customfield_13600": 45,
"customfield_10900": {
"value": "Monitoring/Alerting"
},
"customfield_10471": [
"3-30536161871 (SM-2046076)"
],
"issuetype": {
"name": "Problem Mgmt - Corrective Action"
},
"created": "2022-08-11T04:46:44.000+0000",
"updated": "2022-11-08T22:11:23.000+0000",
"summary": "FA | EJWL-DEV3| ORA-00020: maximum number of processes (10000) exceeded",
"assignee": null
}
}
And, here's the data object I'd like to bind to. I have represented what they should be as jq expressions.
#Serializable
data class MajorIncident constructor(
#SerialName("key")
val id: String, // .key
val created: Instant, // .fields.created
val pillar: String, // .fields.customfield_10443.value
val impactBreadth: String?,
val duration: Duration, // .fields.customfield_13600 as minutes
val detectionSource: String, //.fields.customfield_10900.value
val updated: Instant, // .fields.updated
val assignee: String, // .fields.assignee
// "customfield_10471": [
// "3-30536161871 (SM-2046076)"
// ],
val serviceRequests: List<String>?, // .fields.customfield_10471 | map(split(" ")[0]) -
#SerialName("summary")
val title: String, //.summary
val type: String, // .fields.issuetype.name // what are options?
)
If you're using Kotlinx Serialization, I'm not sure there is any built-in support for jsonpath.
One simple option is to declare your Kotlin model in a way that matches the JSON. If you really want a flattened object, you could convert from the structured model into the flat model from Kotlin.
Another option is to write a custom serializer for your type.

What is the JSON interface in Typescript

Why does the following snippet not compile, and how is the ES6 JSON interface supposed to be used?
let myVar: JSON = {"id": "12"};
Gives the following error message: Type '{ id: string; }' is not assignable to type 'JSON'. Object literal may only specify known properties, and '"id"' does not exist in type 'JSON'.
My IDE gives the following definition for JSON, but I can’t understand it:
interface JSON {
readonly [Symbol.toStringTag]: string;
}
JSON is a global object defined by the JS specification designed to hold the parse and stringify methods for converting between JS data structures and JSON texts.
It isn't a type. It isn't supposed to be used as one.
When creating a custom object format, you are supposed to define your own type (although it isn't useful to do so here, but might be if you define a function elsewhere that you need to pass the object to as an argument). When dealing with JSON, you are dealing with strings.
type MyFormat = {
id: string;
}
let myVar: MyFormat = {"id": "12"};
let myJSON: string = JSON.stringify(myVar);
There's no reason you'd use the JSON interface in your code. It relates to the JSON built-in object, and there's no reason to use that object other than using its parse and/or stringify methods to parse or create JSON text.
From your code, you appear to misunderstand what JSON is. (Lots of people do! :-) ) JSON is a textual notation for data exchange. (More here.) If you're dealing with JavaScript or TypeScript source code, and not dealing with a string, you're not dealing with JSON.
Your myVar refers to an object. There's no need to put a type annotation on it, you can just let TypeScript infer it from the initializer, but if you wanted to put a type annotation on it you'd use either {id: string;} or Record<string, string> or some other object type:
// Letting TypeScript infer
let myVar = {"id": "12"};
// Or specify an object with an `id` property of type string
let myVar: {id: string;} = {"id": "12"};
// Or create a type alias and use it
type MyVarType = {
id: string;
};
let myVar: MyVarType = {"id": "12"};
// Or perhaps an object where any string is a valid property name and the types are all strings
let myVar: Record<string, string> = {"id": "12"};
See the documentation linked above for more about object types.
Side note: If you meant to use a number for id, you'd use id: number or similar:
let myVar: {id: number;} = {id: 12};

how to get value from json via variable in typescript?

supposing I have to read some data from some json files(i18n), every json file may look like:
{
"foo": "1",
"bar": "2",
...
}
I don't know how many fields this json have(it can be expanded), but it's fields look like
{
[prop: string]: string
}
besides, all the json files share the same fields.
when I try to read a value from this json via:
//a can be expanded, I'm not sure how many fileds does it have
let a = {
name: "dd",
addr: "ee",
}
//I'm confident a has a field "name"
let b = "name";
console.log(a[b]);
the error message is:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type
how could I fix it?
The error you're encountering is because the keys in a is not just any string (in fact, it can only be "name" or "add"), but b can be a string of any arbitrary value. If you are very sure that b represents a key found in the object a, you can hint TypeScript as such:
let b: keyof typeof a = "name";
Attempting to assign any arbitrary string value to b will lead to an error:
// This will cause an error
let b: key typeof a = "foobar";
See proof-of-concept on TypeScript Playground.

How parse JSON response with axios and TypeScript to PascalCase model?

I have type like this:
export type Model = {
Id: number,
Name: string
}
and a JSON response like this: {"id": 0, "name": "User"}.
After Axios parsed that response (const response = await Axios.get<Model>(source)), I get next object:
Id: undefined Name: undefined id: 0 name: "User"
How correctly parse response to PascalCase model kind?
`
There are many ways to do this, but whatever happens, you need to change your types, as they're not correct at the moment, and you need to manually transform your result object.
The types currently say that Axios.get will return a model with Id and Name keys, which is definitely wrong (it will return a model with id and name keys). You can transform this result, but can't easily change the first return value there, so you need to correct that.
Once that's correct, you need to transform the JSON response to the model you want. One option is to use lodash, which makes this fairly easy.
A full example might look like this:
export type Model = {
Id: number,
Name: string
}
// Get the raw response, with the real type for that response
const response = await Axios.get<{ id: number, name: string }>(source);
// Transform it into a new object, with the type you want
const model: Model = _.mapKeys(response,
(value, key) => _.upperFirst(key) // Turn camelCase keys into PascalCase
);
There are lots of other ways to do the last transformation step though, this is just one option. You might also want to think about validation first too, to check the response data is the shape you expect, if that's a risk in your case.

Why is The Parameter of a Function Retrieved From an Object "never"

I'd expect this code to work. But instead I get this TypeError.
The idea is that myFunctions holds handlers for data received from JSON.
The JSON objects are either of type A, or of type B. If type is "a" I want param to be handled by the function stored in myFunctions.
This is my approach, but the signature of the retrieved function is never allthough all type information is available.
const myFunctions = {
"a": function(o: string) {return "A"},
"b": function(o: number) {return "B"}
};
interface A {
type: "a"
param: string
}
interface B {
type: "b"
param: number
}
function getIt(i: A | B) {
const p = i.param;
const f = myFunctions[i.type];
// at this point typescript identifies the type of f to be ((o: string) => string) | ((o: number) => string)
return f(p); // <- Argument of type 'string | number' is not assignable to parameter of type 'never'. Type 'string' is not assignable to type 'never'.ts(2345)
}
Can someone explain to me why this behaviour occurs and how to fix it?
Alternatively I'd be happy to hear about other approaches to call the correct handler given a certain JSON object.
It is not possible to do this without introducing new if or switch statements. Typescript can't really follow that f and p are related and consistent with one another. Your use case could probably be helped by something like this proposal but that has been sitting as a proposal for a while so I would not really wait for it.
The issue here is that i.type is "A" | "B", so when using it to index myFunctions you just get back a union of all functions (((o: string) => string) | ((o: number) => string)). But this union of functions is only callable with an argument that is an intersection of all possible arguments. That intersection here is string & number which typescript reduces to never since it is a primitive intersection that can never be inhabited by any value. You can read here about the rules on union invocation.
You can add an if or switch to fix this, although it does make the code redundant:
function getIt(i: A | B) {
switch (i.type) {
case "a": return myFunctions[i.type](i.param)
case "b": return myFunctions[i.type](i.param)
}
}
Playground Link
Or use a type assertion to just make things work:
function getIt(i: A | B) {
const p = i.param;
const f = myFunctions[i.type];
return f(p as never);
}
Playground Link