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
Related
I'm trying to instantiate an instance of a struct (Struct1) in an array. Instances of Struct1 store a function (method) that takes a generic type T for a parameter. The following code is how I attempted to do this:
struct Struct1<T> {
method: fn(T)
}
fn main() {
let arrOfStructs = [
Struct1 {
method: fn(char) {
let a = char; //this does nothing useful, just a mock function
}
}
];
}
But I get the error following errors:
error: expected expression, found keyword `fn`
--> test.rs:8:21
|
7 | Struct1 {
| ------- while parsing this struct
8 | method: fn(char) {
| ^^ expected expression
error[E0063]: missing field `method` in initializer of `Struct1<_>`
--> test.rs:7:9
|
7 | Struct1 {
| ^^^^^^^ missing `method`
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0063`.
I'm assuming the second error listed is present simply because the instance's method wasn't properly instantiated, because of the first listed error. But I can't figure out what the first error is trying to say. As far as I know, Rust instantiations can be implicitly typed. I can't figure out what else might be the problem though. Could you guys help me out with this one? Much appreciated!
The syntax to create an anonymous function (a closure) in Rust is not what you tried. Rather, it is:
|arg1, arg2, arg3, ...| body
Where body can be any expression, including a block (|| { body }), parameters can have type annotations (|arg: Type| {}) and the closure may specify the return type explicitly using ->: || -> ReturnType {}.
In your example,
fn main() {
let arrOfStructs = [
Struct1 {
method: |char: YouHaveToSpecifyTheTypeHere| {
let a = char; //this does nothing useful, just a mock function
}
}
];
}
Just to supplement the answer:
There are two ways to construct a function pointer, one is constructing via an existing function,
and the other is using a closure(anonymous function) that doesn't capture any environmental
variables
fn add_one(x: usize) -> usize {
x + 1
}
// using an existing function
let ptr: fn(usize) -> usize = add_one;
assert_eq!(ptr(5), 6);
// using a closure that does not enclose variables
let clos: fn(usize) -> usize = |x| x + 5;
assert_eq!(clos(5), 10);
link to the official doc
Consider the following interface within TypeScript
interface IApiCall<TResponse> {
method: string;
url: string;
}
Which is then used within the following method;
const call = <TResponse>(api: IApiCall<TResponse>): void => {
// call to API via ajax call
// on response, grab data
// use JSON.parse(data) to convert to json object
return json as TResponse;
};
Now we use this for Type safety within our methods so we know what objects are being returned from the API. However, when we are returning a single string from the API, JSON.parse is converting the string '12345' into a number, which then breaks further down the line when we are trying to treat this as a string and use value.trim() yet it has been translated into a number.
So ideas to solve this so that we are not converting a string into a number.
How can we stop JSON.parse from converting a single string value into a number?
If using JSON.parse, we check the type of TResponse and compare it against the typeof of json generated.
if (typeof (json) !== typeof(TResponse))...
However there doesn't seem to be an obvious way to determine the generic type.
Question 1: How can we stop JSON.parse() from converting a single string value into a number?
JSON is a text format, so in JSON.parse(x), x needs to be a string. But JSON text represents data of not-necessarily-string types. It sounds like you might be making a category mistake, by confusing a thing with its representation.
If you convert the number 12345 to JSON (JSON.stringify(12345)) you will get the string "12345". If you parse that string, (JSON.parse("12345")), you will get the number 12345 back. If you wanted to get the string "12345", you need to encode it as JSON ( JSON.stringify("12345")) as the string "\"12345\"". If you parse that ( JSON.parse('"12345"') you will get the string "12345" out.
So the straightforward answer to the question "How can we stop JSON.parse() from converting a single string value into a number" is "by properly quoting it". But maybe the real problem is that you are using JSON.parse() on something that isn't really JSON at all. If you are given the string "12345" and want to treat it as the string "12345", then you don't want to do anything at all to it... just use it as-is without calling JSON.parse().
Hope that helps. If for some reason either of those don't work for you, you should post more details about your use case as a Minimal, Complete, and Verifiable example.
Question 2: How do we determine that the returned JSON-parsed object matches the generic type?
In TypeScript, the type system exists only at design time and is erased in the emitted JavaScript code that runs later. So you can't access interfaces and type parameters like TResponse at runtime. The general solution to this is to start with the runtime solution (how would you do this in pure JavaScript) and help the compiler infer proper types at design time.
Furthermore, the interface type IApiCall
interface IApiCall<TResponse> {
method: string;
url: string;
}
has no structural dependence on TResponse, which is not recommended. So even if we write good runtime code and try to infer types from it, the compiler will never be able to figure out what TResponse is.
In this case I'd recommend that you make the IApiCall interface include a member which is a type guard function, and then you will have to write your own runtime test for each type you care about. Like this:
interface IApiCall<TResponse> {
method: string;
url: string;
validate: (x: any) => x is TResponse;
}
And here's an example of how to create such a thing for a particular TResponse type:
interface Person {
name: string,
age: number;
}
const personApiCall: IApiCall<Person> = {
method: "GET",
url: "https://example.com/personGrabber",
validate(x): x is Person {
return (typeof x === "object") &&
("name" in x) && (typeof x.name === "string") &&
("age" in x) && (typeof x.age === "number");
}
}
You can see that personApiCall.validate(x) should be a good runtime check for whether or not x matches the Person interface. And then, your call() function can be implemented something like this:
const call = <TResponse>(api: IApiCall<TResponse>): Promise<TResponse | undefined> => {
return fetch(api.url, { method: api.method }).
then(r => r.json()).
then(data => api.validate(data) ? data : undefined);
};
Note that call returns a Promise<Person | undefined> (api calls are probably asynchronous, right? and the undefined is to return something if the validation fails... you can throw an exception instead if you want). Now you can call(personApiCall) and the compiler automatically will understand that the asynchronous result is a Person | undefined:
async function doPersonStuff() {
const person = await call(personApiCall); // no <Person> needed here
if (person) {
// person is known to be of type Person here
console.log(person.name);
console.log(person.age);
} else {
// person is known to be of type undefined here
console.log("File a missing Person report!")
}
}
Okay, I hope those answers give you some direction. Good luck!
Type annotations only exist in TS (TResponse will be nowhere within the output JS), you cannot use them as values. You have to use the type of the actual value, here it should be enough to single out the string, e.g.
if (typeof json == 'string')
I want to write a function with a parameter type guard that accepts the value from a K/V pair from an object or type...
type VodTreeName = {
Movie: 'movie_vod',
TV: 'tv_vod',
VideoStore: 'video_store'
};
function test(something: VodTreeName) {
// expecting something === 'movie_vod'
}
test(VodTreeName.Movie);
// 'VodTreeName' only refers to a type, but is being used as a value here.
--or--
const VodTreeName = {
Movie: 'movie_vod',
TV: 'tv_vod',
VideoStore: 'video_store'
};
function test(something: keyof typeof VodTreeName) {
// expecting something === 'movie_vod'
}
test(VodTreeName.Movie);
// Argument of type 'string' is not assignable to parameter of type '"Movie" | "TV" | "VideoStore"'.
How else can I do this without having a type AND an object that I have to export/import to other modules?
You cannot use a type alias in runtime, there's no js equivalent for that.
The test function in the 2nd snippet expects a key of VodTreeName but you are passing the value, it should be:
function test(key: keyof typeof VodTreeName) {
console.log(VodTreeName[key]);
}
test("Movie");
If you want to use it like so:
test(VodTreeName.Movie);
Then you're basically looking for a string based enum, in which case check this thread: Create an enum with string values in Typescript and this issue: Proposal: String enums.
I am using serde_json to deserialise a json document. I have a function that given a string (this is the json document), will return a serde_json Value (this is an enum that represents the json type), returns an Option.
This value is passed around to other functions as required.
However, I realised that passing around a Value is not quite what I want, because doing this, the key is not available.
To illustrate my point, if I have a json document that looks like this:
{
"root" : {
"regex" : null,
"prefixes" : [ "a_", "b_" ]
}
}
"root" is a json object, "regex" is json Null and "prefixes" is a json array.
Now, the json type Value is an enum with discriminators representing the json types, eg Object, Null, Array for the examples given above.
The serde_json crate uses std::collections::BTreeMap to represent nodes in the json document, where the String type repesents the json keys (in the above, these would be "root", "regex" and "prefixes". So passing around just references to Values is only partly helpful, I should be passing around BTreeMap instead, so that I can access the key too.
So this is the following function that I am trying to re-write:
fn get_json_content(content_s : &str) -> Option<Value> {
// instead of returning a value, we need to return a BTreeMap, so we can get the
// key and the value.
println!("===>>> json_content obtained: {}", content_s);
match serde_json::from_str(content_s) { // -> Result<Value>
Ok(some_value) => Some(some_value),
Err(_) => None
}
}
So I started to re-write the function but became up against the "the type of this value must be known in this context" error:
fn get_json_content_as_btreemap<'a>(content_s : &str) -> Option<&'a BTreeMap<String, Value>> {
match serde_json::from_str(content_s) { // -> Result<Value>
Ok(some) => {
// I expect the type of key_value_pair to be BTreeMap<String, Value>>
// (but I may be wrong!)
let key_value_pair = some.as_object().unwrap(); // Error here
},
Err(_) => None
}
}
I found other questions on stackoverflow like this one:
the type of this value must be known in this context
and using this as a helper, I tried to insert the type as follows:
let key_value_pair = some.as_object::<BTreeMap<_, _>>().unwrap();
which doesnt fix the issue. Also, tried other similar variations to no avail. So how do I fix this please?
EDIT:
I have another function in this app as follows:
fn get_root_value<'a>(json_documemt : &'a Value) -> Result<&'a Value, JsonErrorCode> {
if json_documemt.is_object() {
for (k, v) in json_documemt.as_object().unwrap().iter() {
if k == "root" {
println!("found root: {}", k);
return Ok(v)
}
}
return Err(JsonErrorCode::Custom("Failed to find root node".to_string()))
}
Err(JsonErrorCode::Custom("Not an object".to_string()))
}
... and this works fine. Here you can see that I can call as_object() and then obtain the key and value as a tuple pair. I don't understand why as_object is working in one case but not the other. I would like to pull out the BTreeMap and pass this around as a borrowed item.
You can change the return type of your initial function and serde_json will deserialize to the appropriate object if it can:
fn get_json_content(content_s : &str) -> Option<BTreeMap<String, Value>> {
// instead of returning a value, we need to return a BTreeMap, so we can get the
// key and the value.
println!("===>>> json_content obtained: {}", content_s);
match serde_json::from_str(content_s) { // -> Result<Value>
Ok(some_value) => Some(some_value),
Err(_) => None
}
// Note: this match statement can be rewritten as
// serde_json::from_str(content_s).ok()
}
Your second example won't work because you are instantiating the Value object inside the function, and then trying to return a reference to the object you just instantiated. This won't work because the object will go out of scope at the end of the function and the reference will then be invalid.
I am relying on rustc_serialize to parse JSON data into a struct, Orders, which represents a Vec of Order structs. The JSON data may have an array or a null value; my intent is to either parse the array of orders normally, if any, or parse the null value as an Orders with an empty Vec. If neither of these is the case, then an error is to be relayed. This is my attempt:
impl Decodable for Orders {
fn decode<D: Decoder>(d: &mut D) -> Result<Self, D::Error> {
let try = d.read_seq(|d, l| {
let mut orders = Vec::new();
for _ in 0..l {
let order = try!(Decodable::decode(d));
orders.push(order);
}
Ok(Orders(orders))
});
match try {
value # Ok(_) => value,
error # Err(e) => match e {
ExpectedError(_, x) if &x == "null" => Ok(Orders(Vec::new())),
_ => error,
},
}
}
}
My issue has to do with pattern matching on ExpectedError. The compiler gives me the following error:
expected `<D as rustc_serialize::serialize::Decoder>::Error`,
found `rustc_serialize::json::DecoderError`
(expected associated type,
found enum `rustc_serialize::json::DecoderError`) [E0308]
src/api/types/json.rs:125 ExpectedError(_, x) if &x == "null" => Ok(Orders(Vec::new())),
^~~~~~~~~~~~~~~~~~~
I am stumped on this one. How can I correct this?
How can I correct this?
In general, you would have to choose between being generic or specialized. You cannot pattern match on an associated type because this type can be anything and a generic method should work for any type which satisfies the constraints.
For example, in your case:
<D as rustc_serialize::serialize::Decoder>::Error can be anything
rustc_serialize::json::DecoderError is but one possibility
So you should normally choose between using some abstract D or specializing the decoding for json.
However, since you are implementing Decodable here, you cannot choose NOT to be generic as you do not get to pick the signature.
Furthermore, it appears that rustc_serialize does not support tentative parsing (or at least, its json implementation does not), so I am afraid that you cannot check for nullity (using d.read_nil()) beforehand.
I suppose those limitations can be seen as the reason that this library is being retired in favor of serde, and can only encourage you to try it out.