I need to cast a variable where I store an object depending on some conditions.
I have this var:
class MyClass {
public referrers: SelectItemGroup[];
}
This type is an object and you have to provide a label, items and optionally a value. The trouble comes when depending on some stuff I need to add only a few elements to this array and they don't have items so I need to cast this.referrers from SelectItemGroup[] to SelectItem[] and I am not really sure of how to do this.
I tried this.referrers = <SelectItem[]>this.referrers; which obviously doesn't work.
The 'casting' operator you used is called a type assertion in typescript, the reason it's called an assertion is that as opposed to a casting from other languages it has no runtime behavior (no conversion is done, no runtime error occurs if the value is incompatible with the asserted type). Type assertions are useful when you have more information then the compiler about the type of value and want to let the compiler know about it.
If this case using this assertion would let you access operations of the referrers array typed with SelectItem. So this code should work:
class MyClass {
public referrers: SelectItemGroup[];
method() {
let s: SelectItem = this.createSelectItem();
(<SelectItem[]>this.referrers).push(s)
}
}
Now the array will contain both SelectItemGroup and SelectItem but we should let the compiler know about this. Typescript supports a feature called union types which allows us to specify a type is either one of two other types (ex: A | B) so we could type the referrers as an array of such a union type, making the assertion unecessary:
class MyClass {
public referrers: (SelectItemGroup | SelectItem)[];
method() {
let s: SelectItem = this.createSelectItem();
this.referrers.push(s);
}
}
When accessing members of referrers we now have a different problem, since an item can be either SelectItemGroup or SelectItem the compiler will only let us access common members:
interface SelectItemGroup{
name: string;
children: SelectItem[]
}
interface SelectItem{
name: string;
value: string;
}
class MyClass {
public referrers: (SelectItemGroup | SelectItem)[];
method() {
this.referrers[0].name // valid it's common
this.referrers[0].value // invalid belongs only to SelectItem
this.referrers[0].children // invalid belongs only to SelectItemGroup
}
}
To narrow the type of the element to one or the other type we need to use a type guard. Typescript offers several types of type guards and you can read more about them here as well. The most convenient in this case would be an in type guard which will determine the type based on the existence of a particular field:
class MyClass {
public referrers: (SelectItemGroup | SelectItem)[];
method() {
// type-guards will not work directly on the array we need to put the value in a local variable
let item = this.referrers[0] // item is SelectItemGroup | SelectItem so only name is accessible
if('children' in item) {
item.children // valid item is SelectItemGroup
}else{
item.value // valid item is SelectItem by exclusion
}
}
}
Related
struct Payload {}
struct User {}
struct Post {}
protocol Mapper {
associatedtype PayloadType
associatedtype ResultType
func map(_ payload: PayloadType) -> ResultType
}
class AnyMapper<P, R>: Mapper {
private var _mapClouser: (P) -> R
init<M: Mapper>(_ mapper: M) where M.PayloadType == P, M.ResultType == R {
_mapClouser = mapper.map(_:)
}
func map(_ payload: P) -> R {
_mapClouser(payload)
}
}
class UserMapper: Mapper {
func map(_ payload: Payload) -> User {
return User()
}
}
class PostsMapper: Mapper {
func map(_ payload: Payload) -> [Post] {
return [Post(), Post(), Post()]
}
}
let userMapper = AnyMapper(UserMapper())
let postsMapper = AnyMapper(PostsMapper())
var array: [AnyMapper] = [userMapper, postsMapper] <<< Error: Cannot convert value of type 'AnyMapper<Payload, [Post]>' to expected element type 'AnyMapper<Payload, User>'
I tried to put 2 objects into array, but I get this error: Cannot convert value of type 'AnyMapper<Payload, [Post]>' to expected element type 'AnyMapper<Payload, User>'
Can someone explain to me how to fix it?
AnyMapper by itself is not a type. It is a type constructor. Essentially, it is a “function” that
runs at compile time,
takes types as arguments named P and R, and
returns a type.
So for example if you say AnyMapper<Payload, User>, that's a compile-time function call to the type constructor AnyMapper, passing the arguments Payload and User. It returns a type, which we just refer to using the same syntax, AnyMapper<Payload, User>.
Often, Swift can deduce the argument types, so although you don't pass them explicitly, Swift passes them for you.
That's what's happening on these two lines:
let userMapper = AnyMapper(UserMapper())
let postsMapper = AnyMapper(PostsMapper())
Each of those lines “calls“ AnyMapper after deducing the P and R arguments. We can make the P and R arguments explicit, and also make the returned types explicit, like this:
let userMapper: AnyMapper<Payload, User> = AnyMapper<Payload, User>(UserMapper())
let postsMapper: AnyMapper<Payload, [Post]> = AnyMapper<Payload, [Post]>(PostsMapper())
So now we can see that you're creating two objects of two different types.
You next write this:
var array: [AnyMapper] = [userMapper, postsMapper]
Since you're not explicitly passing type arguments to AnyMapper, you're asking Swift to deduce the arguments. It does so by looking at the first element of the array literal, userMapper. Based on that, it deduces P = Payload and R = User, so it acts like you wrote this:
var array: [AnyMapper<Payload, User>] = [userMapper, postsMapper]
But postsMapper is not an AnyMapper<Payload, User>, so Swift can't compile that statement.
What all this means is that AnyMapper erases only the wrapped Mapper's specific type, but not its PayloadType and ResultType associated types.
It's not clear why you would want to put userMapper and postsMapper in the same array anyway. They have different result types, so you wouldn't be able to treat their results generically. Perhaps if you explain why you think you need to put them both in a single array, we can give you better guidance.
[Post] and User are not the same type. You don't need an erasing type to put them into the same array.
var array: [any Mapper] = [UserMapper(), PostsMapper()]
How to Override push into Array depending on the type?
I am trying something like this
interface Foo{
id:number
}
interface Bar{
name:string
}
public func1( array_var: Foo[] | Bar []){
if(array_var instanceof Array<Foo> ){
array_var.push({id:10})
}
else if(array_var instanceof Array<Bar>){
array_var.push({name:"Stack"})
}
}
Don't forget that types get erased when the TypeScript code is being transpiled to JavaScript, so you cannot have conditions that rely on the type. In JavaScript there is no such thing as Array<T>, there is just Array. For example, [] is both a Foo[] and a Bar[], and we can't tell the difference between the two at runtime because there's no type information left.
In this case you have to leverage type predicates to convince the compiler that you've checked what can be checked:
interface Foo {
id: number
}
interface Bar {
name: string
}
function func1(array_var: Foo[] | Bar[]) {
if (isFooArray(array_var)) {
array_var.push({ id: 10 })
}
else {
array_var.push({ name: "Stack" })
}
}
function isFooArray(arr: any[]): arr is Foo[] {
return arr[0].id !== undefined; // Possible implementation
}
TypeScript playground
It works with a special function which you have to write the implementation, and TypeScript considers that the return value of that function (a boolean) represents the result of the arr is Foo[] expression. That helps it narrow the types accordingly. Note again per the first paragraph that you need some tangible runtime value to operate on: you'll have to make a decision as the developer on how to handle the empty array case.
I wrote an example implementation of it but you could write anything inside that type predicate function.
I recently updated typescript and lodash, and now the compiler understands that e.g. methods like _.last() will return a value if the array is non-empty and undefined if it's empty. This is expected behavior as such, especially if we don't know if the array is empty or not.
However, in cases such as calling _.last([1,2,3]), we just know that the array is non-empty and this will always return a number. Moreover, I have several places in the code where I have checked for non-emptiness, and act accordingly, like:
if (!_.isEmpty(array)) {
return _.last(array);
}
... where by just looking at that code, we know the statement will return an element with the type of an element in that array. But the return type is T | undefined nevertheless.
I know that I can just cast the value in cases like this. But I'd rather avoid casting things.
So my question is: could it be possible for typescript to understand situations like these?
We can create a system where isEmpty adds information to the type of the array. This can be a type we can call HasElements<T> where T can true or false. If the type is still untested, it should have both possibilities in it's type (T[] & (HasElements<true> | HasElements<false>)). This has to be added manually to the type unfortunately.
import _ from 'lodash'
declare module 'lodash' {
type HasElements<T extends boolean> = T extends boolean ? { // distributive conditional, makes HasElements<boolean> == HasElements<true> | HasElements<false>
"gurad-traits"?: {
hasElements?: T
}
}: never
interface LoDashStatic {
isEmpty<T extends HasElements<boolean>>(value?: T): value is T & HasElements<false>;
last<T> (array: List<T> & HasElements<true>): T;
last<T> (array: (List<T> & HasElements<false> )| null | undefined): undefined;
}
}
function test<T>(array: T[] & _.HasElements<boolean>, defaultValue: T): T {
if (!_.isEmpty(array)) {
return _.last(array);
}else {
let u: T = _.last(array); /// err, returns undefined
return defaultValue;
}
}
test([1,2,3], 1); //HasElements does not influence the assignability of arrays
I have a function with the following signature
public async sequenceAnimations(animations: AnimationPlayer[] | AnimationTracker[]): Promise<any>
In the function itself I want to branch based on if it is an AnimationPlayer array or an AnimationTracker array so I tried this:
let mappedAnimations = animations;
if (animations instanceof Array<AnimationTracker>) {
mappedAnimations = animations.map(anim => anim.animationPlayer)
}
As you can see I am trying to allow the caller to pass either an AnimationPlayer array or an AnimationTracker array which has an instance of animationPlayer. But I get an error when checking instanceof the Array with a type
The right-hand side of an 'instanceof' expression must be of type 'any' or of a type assignable to the 'Function' interface type.
Also the autocomplete isn't registering the type of the array in the if-block so I assume I can't check the array type like this.
What is the proper way to determine what the type of array being passed is?
You can't use instanceof with a generic type with type parameters. After compilation all generics are erased so animations instanceof Array<AnimationTracker> would turn into animations instanceof Array which would not do what you expect it to do.
Since in Javscript arrays are not typed there is no build-in way of differentiate between AnimationPlayer[] and AnimationTracker[] and if the arrays are empty, at runtime they are really indistinguishable. You can however create a custom type guard that uses the first non-null item in the array to determine the type. For empty arrays this would always return false but it might be an ok solutions in most cases:
function isArrayOf<T>(array:any[], cls: new (...args: any[]) => T) : array is T[] {
for(let item of array) {
if(item != null) return item instanceof cls;
}
return false;
}
async function sequenceAnimations(animations: AnimationPlayer[] | AnimationTracker[]): Promise<any> {
let mappedAnimations = animations;
if (isArrayOf(animations, AnimationTracker)) {
// animations is AnimationTracker[]
mappedAnimations = animations.map(anim => anim.animationPlayer);
}
}
Short answer: You can't.
The main idea of TypeScript is to add types at compile time and emit plain JavaScript code after compilation. JavaScript itself has no support for advanced type checking and thus you're only option is to do duck-typing at runtime.
How can I add a method to a typed array like Array<MyClass>?
Consider a case where you have a typed array where it might make sense to offer a property/method to calculate a value by accessing all items of the array.
class Foo {
date: Date
}
class FooArray extends Array<Foo> {
dateInterval() {
// Some algorithm that iterates over all dates to find
// min/max
return this.reduce(..)
}
}
But something tells me that I'm going the wrong way. For example, FooArray.splice() returns the type Foo[], not FooArray, which makes total sense to me.
Can anyone point me into the right direction?
I'll provide 2 options for you
Casting
Just explicitly add the splice method to override the inherited method to return your preferred type, with casts
splice() {
let arr = super.splice();
return new FooArray(arr); // or some other way of casting
}
Alternatively, wrapping
Wrapping
class FooArray {
constructor(private arr: Foo[]) { /* maybe copy the array...? */ }
splice(start: number, end?: number) {
return new FooArray(this.arr.splice(start, end));
}
}
This way you must be explicit about what you expose, rather than mixing the inherited base class' methods that will return normal arrays. Take your pick.