TypeScript: function expression overloads - function

I have this TypeScript code which uses overloads on a function declaration. This code works as expected.
function identity(x: string): string;
function identity(x: number): number;
function identity(x: string | number): string | number {
return x;
}
const a = identity('foo') // string
const b = identity(1) // number
const c = identity({}) // type error (expected)
I am trying to achieve the equivalent of this using function expressions instead of function declarations, however I get a type error:
/* Type '(x: string | number) => string | number' is not assignable to type '{ (x: string): string; (x: number): number; }'.
Type 'string | number' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string' */
const identity: {
(x: string): string;
(x: number): number;
} = (x: string | number): string | number => x;
I want to know how I can achieve the same effect of overloading the function but with function expressions.

You can use a type assertion on the function implementation instead. In an assignment the checks are stricter for compatibility, with an assertion they are weaker. Despite this, we still get a decent amount of type safety (I am not sure it is equivalent to the overloads to implementation signature checks but it seems pretty close):
//OK
const identity = ((x: string | number): string | number => x) as {
(x: string): string;
(x: number): number;
};
// Error argument is incompatible
const identity2 = ((x: boolean): string | number => x) as {
(x: string): string;
(x: number): number;
};
// Error return type is incompatible
const identity3 = ((x: string | number) => false) as {
(x: string): string;
(x: number): number;
};

Related

Typescript: interface that extends a JSON type

I am using a generic JSON type in typescript, suggested from here
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| {[key: string]: JSONValue}
I want to be able to cast from interface types that match JSON to and from the JSON type. For example:
interface Foo {
name: 'FOO',
fooProp: string
}
interface Bar {
name: 'BAR',
barProp: number;
}
const genericCall = (data: {[key: string]: JSONValue}): Foo | Bar | null => {
if ('name' in data && data['name'] === 'FOO')
return data as Foo;
else if ('name' in data && data['name'] === 'BAR')
return data as Bar;
return null;
}
This currently fails because Typescript does not see how the interface could be of the same type as JSONValue:
Conversion of type '{ [key: string]: JSONValue; }' to type 'Foo' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
Property 'name' is missing in type '{ [key: string]: JSONValue; }' but required in type 'Foo'.
but analytically we of course know this is ok, because we recognize that at runtime types Foo and Bar are JSON compatible. How do I tell typescript that this is an ok cast?
ETA: I can follow the error message and cast to unknown first, but I'd rather not do that -- it would be better if TS actually understood the difference, and I'm wondering if it's possible at all.
The issue here is that the compiler does not use the check if ('name' in data && data['name'] === 'FOO') to narrow the type of data from its original type of {[key: string]: JSONValue}. The type {[key: string]: JSONValue} is not a union, and currently in operator checks only narrow values of union types. There is an open feature request at microsoft/TypeScript#21732 to do such narrowing, but for now it's not part of the language.
That means data stays of type {[key: string]: JSONValue} after the check. When you then try to assert that data is of type Foo via data as Foo, the compiler warns you that you might be making a mistake, because it doesn't see Foo and {[key: string]: JSONValue} are types that are related enough.
If you are sure that what you're doing is a good check, you could always do with the compiler suggests and type-assert to an intermediate type which is related to both Foo and {[key: string]: JSONValue}, such as unknown:
return data as unknown as Foo; // okay
If that concerns you then you can write your own user defined type guard function which performs the sort of narrowing you expect from if ('name' in data && data['name'] === 'FOO'). Essentially if that check passes, then we know that data is of type {name: 'FOO'}, which is related enough to Foo for a type assertion. Here's a possible type guard function:
function hasKeyVal<K extends PropertyKey, V extends string | number |
boolean | null | undefined | bigint>(
obj: any, k: K, v: V): obj is { [P in K]: V } {
return obj && obj[k] === v;
}
So instead of if ('name' in data && data['name'] === 'FOO'), you write if (hasKeyVal(data, 'name', 'FOO')). The return type obj is {[P in K]: V} means that if the function returns true, the compiler should narrow the type of obj to something with a property whose key is of type K and whose value is of type V. Let's test it:
const genericCall = (data: { [key: string]: JSONValue }): Foo | Bar | null => {
if (hasKeyVal(data, 'name', 'FOO'))
return data as Foo; // okay, data is now {name: 'FOO'} which is related to Foo
else if (hasKeyVal(data, 'name', 'BAR'))
return data as Bar; // okay, data is now {name: 'BAR'} which is related to Bar
return null;
}
Now it works. The hasKeyVal() check narrows data to something with a name property of the right type, and this is related enough to Foo or Bar for the type assertion to succeed (the type assertion is still necessary because a value of type {name: 'Foo'} might not be a Foo if Foo has other properties).
Playground link to code

How to define a JSON-compatible parameter to avoid error "Index signature is missing"?

I've got a function defined as:
export function useSubmitHandler(url: string, data: Json): [FormEventHandler<HTMLFormElement>, boolean] {}
Where Json is:
type JsonPrimitive = string | number | boolean | null | undefined
interface JsonMap extends Record<string, JsonPrimitive | JsonArray | JsonMap> {}
interface JsonArray extends Array<JsonPrimitive | JsonArray | JsonMap> {}
export type Json = JsonPrimitive | JsonMap | JsonArray
If I try to call it with an arbitrary interface though, I get an error:
TS2345: Argument of type 'Fee' is not assignable to parameter of type 'Json'.   
Type 'Fee' is not assignable to type 'JsonMap'.     
Index signature is missing in type 'Fee'
But if I call it with that same object but spread like {...store.data} then the error goes away.
How can I type useSubmitHandler properly so that it will accept any object that is JSON stringifyable?
I think the Json type is correct, but it needs something more to allow passing arbitrary types into the function.
Fee is:
interface Fee {
id: number|null;
name: string;
type: string;
amount: string;
default: number;
company_id: number;
deleted_at?: any;
active: number;
fee_or_discount: string;
}
Of course, I'd like this to work with any type.
Option 1: Redefine Json type
type JsonPrimitive = string | number | boolean | null;
type JsonMap = {
[key: string]: JsonPrimitive | JsonMap | JsonArray;
}
type JsonArray = Array<JsonPrimitive | JsonMap | JsonArray>;
type Json = JsonPrimitive | JsonMap | JsonArray;
Option 2: Add an index signature to the Fee interface
interface Fee {
[property: string]: any;
id: number|null;
name: string;
type: string;
amount: string;
default: number;
company_id: number;
deleted_at?: any;
active: number;
fee_or_discount: string;
}
Option 3: Add an inline type assertion for the index signature, such as:
useSubmitHandler(Router.route('fees.store')!, store.data as {[property: string]: any})
See also
TypeScript GitHub issue Please provide a json basic type #1897
One possible way to handle this is to wrap store.data in a function which applies an index signature.
const indexed = <T extends {}>(obj: T): T & {[key: string]: never} => obj;
This function returns the same object that it was given but adds additional typescript information. We keep all of the values of the previously known type T intact, but add an index signature {[key: string]: never} which tells typescript that any keys other than those in the type cannot be anything other than undefined.
You can call your handler like this
useSubmitHandler(url, indexed(store.data))
Playground Link
The other option is to change your definition of Json such that it doesn't have the index signature, which comes from using Record. But I'm assuming that would be less desirable since (I think) you would have to broaden what's allowed.

json2typescript serializeObject method returning empty object

I am trying to convert Typescript Contact class back to JSON which is possible according to the documentation for the 'json2typscript' package.
deserializeObject works fine.
serializeObject returns an empty obj.
map(res => {
const jsonConvert: JsonConvert = new JsonConvert();
return jsonConvert.deserializeObject(res, Contact);
})
This does not work by returning {}:
try {
const obj = new JsonConvert().serializeObject(contact);
console.log(obj);
} catch (err) {
console.error(err);
}
This is the class I am trying to map against:
#JsonObject('Contact')
export class Contact {
#JsonProperty('id', String, true)
id: string = undefined;
#JsonProperty('created', String, true)
created: string = undefined;
#JsonProperty('updated', String, true)
updated: string = undefined;
#JsonProperty('first_name', String, true)
firstName: string = undefined;
#JsonProperty('last_name', String, true)
lastName: string = undefined;
#JsonProperty('role', String, true)
role: string = undefined;
#JsonProperty('phone', String, true)
phone: string = undefined;
#JsonProperty('email', String, true)
email: string = undefined;
#JsonProperty('notes', String, true)
notes: string = undefined;
#JsonProperty('companies', [Company])
companies: [] = undefined;
}
Any ideas on how to do this?
As this Pull Request
If you are using an object that is not a real instance of the mapped class you should pass a second argument to serializeObject as a reference class:
jsonConvert.serializeObject(contact, Contact);
Works on v1.3.0+

ScalaJson implicit Write, found: Any required: play.api.libs.json.Json.JsValueWrapper

I am building a web app using Scala / Play Framework and Reactive Mongo and I want the models to be defined in the database instead of having them hardcoded.
To do so, I am writing a class EntityInstance taking a Sequence of FieldInstance :
case class EntityInstance(fields: Seq[FieldInstance])
I am trying to accept fields from any types and to convert them to Json : example
new FieldInstance("name", "John") | json: { "name": "John" }
new FieldInstance("age", 18) | json: { "age": 18 }
At the moment I am trying to accept Strings, Booleans and Integers and if the type is not supported I write some error :
new FieldInstance("profilePicture", new Picture("john.jpg") | json: { "profilePicture": "Unsupported type
I wrote a FieldInstance class taking a fieldName as a String and a value as any type. As soon as that class is instantiated I cast the value to a known type or to the String describing the error.
class FieldInstance(fieldNamec: String, valuec: Any) {
val fieldName = fieldNamec
val value = valuec match {
case v: Int => v
case v: String => v
case v: Boolean => v
case _ => "Unrecognized type"
}
}
object FieldInstance {
implicit val fieldInstanceWrites = new Writes[FieldInstance] {
def writes(fieldInstance: FieldInstance) = Json.obj(
fieldInstance.fieldName -> fieldInstance.value
)
}
}
I created a companion object with an implicit Write to json so I can call "Json.toJson()" on an instance of FieldInstance and get a json as described on my examples above.
I get an error : found: Any required: play.api.libs.json.Json.JsValueWrapper
I understand that it comes from the fact that my value is of type Any but I thought the cast would change that Any to String || Boolean || Int before hitting the Writer.
PS: Ignore the bad naming of the classes, I could not name EntityInstance and FieldInstance, Entity and Field because these as the classes I use to describe my models.
I found a fix to my problem :
The type matching that I was doing in the class should be done in the implicit Write !
class FieldInstance(fieldNamec: String, valuec: Any) {
val fieldName = fieldNamec
val value = valuec
override def toString(): String = "(" + fieldName + "," + value + ")";
}
object FieldInstance {
implicit val fieldInstanceWrites = new Writes[FieldInstance] {
def writes(fieldInstance: FieldInstance) =
fieldInstance.value match {
case v: Int => Json.obj(fieldInstance.fieldName -> v.asInstanceOf[Int])
case v: String => Json.obj(fieldInstance.fieldName -> v.asInstanceOf[String])
case v: Boolean => Json.obj(fieldInstance.fieldName -> v.asInstanceOf[Boolean])
case _ => Json.obj(fieldInstance.fieldName -> "Unsupported type")
}
}
}
This code now allows a user to create an EntityInstance with Fields of Any type :
val ei = new EntityInstance(Seq[FieldInstance](new FieldInstance("name", "George"), new FieldInstance("age", 25), new FieldInstance("married", true)))
println("-- TEST ENTITY INSTANCE TO JSON --")
println(Json.toJson(ei))
prints : {"entity":[{"name":"George"},{"age":25},{"married":true}]}
Here is my EntityInstance code if you are trying to test it :
case class EntityInstance(fields: Seq[FieldInstance])
object EntityInstance {
implicit val EntityInstanceWrites = new Writes[EntityInstance] {
def writes(entityInstance: EntityInstance) =
Json.obj("entity" -> entityInstance.fields)
}
}
It is returning a String, Int or Boolean but Json.obj is expecting the value parameter of type (String, JsValueWrapper)
def obj(fields: (String, JsValueWrapper)*): JsObject = JsObject(fields.map(f => (f._1, f._2.asInstanceOf[JsValueWrapperImpl].field)))
a quick fix could be to convert the matched value v with toJson provided the implicit Writes[T] for type T is available (which they are for String, Int and Boolean)
class FieldInstance(fieldNamec: String, valuec: Any) {
val fieldName = fieldNamec
val value = valuec match {
case v: Int => Json.toJson(v)
case v: String => Json.toJson(v)
case v: Boolean => Json.toJson(v)
case _ => Json.toJson("Unrecognized type")
}
}
If you'd like to see which DefaultWrites are available you can browse them in the play.api.libs.json package in trait DefaultWrites
for example:
/**
* Serializer for Boolean types.
*/
implicit object BooleanWrites extends Writes[Boolean] {
def writes(o: Boolean) = JsBoolean(o)
}

scala - Passing a function that takes another function as a parameter

I am trying to pass a function as a parameter, but that function has multiple arguments (one of which is a function).
Here is what I am trying to do in a basic Python example:
def first(string1, string2, func):
func(string1, string2, third)
def second(string1, string2, func):
func(string1, string2)
def third(string1, string):
# operations go here
first("one", "two", second)
My attempt at this in Scala was the following:
def first(string1: String, string2: String, func: (Any, Any, Any) => Unit) = {
func(string1, string2, func)
}
def second(string1: String, string2: String, func: (Any, Any) => Unit) = {
func(string1, string2)
}
def third(string1: String, string2: String) = {
// operations
}
def main(args: Array[String]): Unit = {
first("one", "two", second)
}
I get an error for trying to pass second into first with an insufficient amount of arguments. Is it possible to achieve this functionality in the same style as the Python example?
EDIT:
I tried replacing the body of my main method with first("one", "two", second _) and it gives me a type mismatch error
type mismatch; found : (String, String, (Any, Any, Any) => Unit) => Unit required: (Any, Any, Any) =>
Unit
Any idea what's going on here?
What you are trying to do is not type-safe. You cannot assign (String, String, (Any, Any) => Unit) => Unit to (Any, Any, Any) => Unit. If you could, then you could do the following:
val f = second _
val af: (Any, Any, Any) => Unit = f
af(1, "abc", 5)
You can do it if you specify the types more precisely:
def second(string1: String, string2: String, func: (String, String) => Unit) = {
func(string1, string2)
}
def third(string1: String, string2: String) = {
// operations
}
def first(string1: String, string2: String, func: (String, String, (String, String) => Unit) => Unit) = {
func(string1, string2, third)
}