How to Get a Class with Generic Type Accept an Array of Different by Same Generic Types

How to get a class with generic type accept an array of different by same generic types?

It's good that you want to experiment with generics, but this isn't the occasion for it. You say:

The idea ... is to support other items in the future in case I want to expand the app...not just coffee.

But you don't need a generic for that. The only requirement for managing an order is that the order's item be Priceable. Priceable is already a type; you don't need to add a generic type to the mix.

struct Order {
var name: String
var item: Priceable
}

class OrderManager {
private var orders: [Order]

init(orders: [Order]) {
self.orders = orders
}

func add(_ order: Order) {
self.orders.append(order)
}
}

Array of different generics

Jon Skeet's info aside, you might be able to do something like this:

public MyGeneric<T2> ToOtherType<T2>()
{
if (typeof(T2).IsAssignableFrom(typeof(T)))
{
// todo: get the object
return null;
}
else
throw new ArgumentException();
}

new MyGeneric<Dog>().ToOtherType<Animal>(),
new MyInheritedGeneric<Cat>().ToOtherType<Animal>()

Defining an array of differing generic types in TypeScript

This is practically the canonical use case for existential generic types, which are not directly supported in TypeScript (neither are they directly supported in most languages with generics, so it's not a particular shortcoming of TypeScript). There is an open feature request, microsoft/TypeScript#14466, asking for this, but it is not part of the language as of TS4.1.

Generics in TypeScript are "universal", meaning that when I say class Foo<T> {...} I mean that it works for all possible type parameters T. That lets the consumer of a Foo<T> specify the value for T and do what they want with it, while the provider of Foo<T> needs to allow for all possibilities.

Heterogeneous collections like the one you are trying to describe require "existential" generics. In some sense you want interface Instruction<exists T> {...} to mean that there is a type parameter T for which it works. Meaning that the provider of an Instruction could specify the value for T and do what they want with it, while the consumer of an Instruction needs to allow for all possibilities.

For more information about universally-vs-existentially quantified generics, see this SO question and answer.


While there is no direct support for existentials in TypeScript, there is indirect support. The difference between a universal and an existential has to do with who is looking at the type. If you switch the role of producer and consumer, you get existential-like behavior. This can be accomplished via callbacks. So existentials can be encoded in TypeScript.

Let's look at how we might do it for Instruction. First, let's define Instruction as a universal generic, the "standalone" version you mentioned (and I'm removing the JQuery dependency in this code):

interface Instruction<T> {
promise: Promise<T>,
callback?: (data: T) => void
}

Here's the existential encoding, SomeInstruction:

type SomeInstruction = <R>(cb: <T>(instruction: Instruction<T>) => R) => R;

A SomeInstruction is a function that calls a function that accepts an Instruction<T> for any T and returns the result. Notice how SomeInstruction does not itself depend on T anymore. You might wonder how to get a SomeInstruction, but this is also fairly straightforward. Let's make a helper function that turns any Instruction<T> into a SomeInstruction:

const someInstruction = <T,>(i: Instruction<T>): SomeInstruction => cb => cb(i);

Finally we can make your hetereogeneous collection:

const arr: SomeInstruction[] = [
someInstruction({
promise: Promise.resolve({ foo: 'bar' }),
callback: (data) => console.log(data.foo)
}),
someInstruction({
promise: Promise.resolve({ bar: 'foo' }),
callback: (data) => console.log(data.bar)
})
]

That all type checks, as desired.


Actually using a SomeInstruction is a bit more involved than using an Instruction<T>, since it takes a callback. But it's not terrible, and again, allows the T type parameter to appear in a way that the consumer doesn't know what the actual T type is and therefore has to treat it as any possible T:

// writing out T for explicitness here
arr.forEach(someInstruction => someInstruction(<T,>(i: Instruction<T>) => {
i.promise.then(i.callback); // works
}))

// but it is not necessary:
arr.forEach(someInstruction => someInstruction(i => {
i.promise.then(i.callback); // works
}))

Nice.


There are other workarounds, but an existential is what you really want. For completeness, here are some possible workarounds that I will mention but not implement:

  • give up on type safety and use any or unknown and use type assertions to get back the types you want. Blecch.

  • Use a mapped tuple type where you convert a type like [T1, T2, T3] to the corresponding [Instruction<T1>, Instruction<T2>, Instruction<T3>] type. This won't work with push() though, so you'd need to work around that somehow too.

  • Refactor Instruction so as not to need/expose generics. Whatever the consumer plans to do with an Instruction<T> it has to be independent of T (e.g., I can write i.promise.then(i.callback) but I can't do much else, right?), so make an Instruction generic function which requires a valid promise-and-callback pair to create, and returns something non-generic with whatever functionality you need and that's it. In some sense this is a "stripped-down" existential that does not allow the consumer to access the internal promise-callback pair separately.


Playground link to code

Creating an array to store generic types in Java

Question 1:

Basically, this is forbidden by Java language. This is covered in Java Language Specification for generics.

When you use

ArrayList<Integer>[] pl2 = new ArrayList[10];    // warning

you get the compiler warning, because the following example will compile (generating warning for every line of code):

ArrayList wrongRawArrayList = new ArrayList();      // warning
wrongRawArrayList.add("string1"); // warning
wrongRawArrayList.add("string2"); // warning

pl2[0] = wrongRawArrayList; // warning

but now you array, that supposed to contain ArrayList of Integer, contains totally wrong ArrayList of String objects.

Question 2:

As it was already answered, declaration of p12 provides you with compile time checking and frees you from using casting when getting items from your ArrayList.

Slightly modified previous example:

ArrayList<Integer>[] pl2 = new ArrayList[10];                // warning 

ArrayList<String> wrongArrayList = new ArrayList<String>(); // OK!
wrongArrayList.add("string1"); // OK!
wrongArrayList.add("string2"); // OK!

pl2[0] = wrongArrayList; // ERROR

Now, since you are using generics, this won't compile.
But if you use

ArrayList[] pl2 = new ArrayList[10]; 

you will get the same result as in the first example.

Typescript: Using an array of generics to create an array of object with generic in the order of the generic list

You need to create a mapped type from T where there indexes of the array are the keys, and the values are the class wrapping the entry at that index.

class Class<T extends any[]> {
public readonly parameter: {
[Index in keyof T]: OtherClass<T[Index]>
}
}

Now the rest works just like you expect:

const c = new Class<[string, number]>();
const a1: string = c.parameter[0].something;
const a2: number = c.parameter[1].something;

const c1 = new Class<[number, string, SomeClass, SomeOtherClass]>();
const b1: number = c1.parameter[0].something;
const b2: string = c1.parameter[1].something;
const b3: SomeClass = c1.parameter[2].something;
const b4: SomeOtherClass = c1.parameter[3].something;

Playground

How to get a type with multiple generic types to match the type parameters?

You can do the trick if you delegate type check to the method:

private class TypedMap {
private Map<Class<?>, List<?>> map = new HashMap<>();

public <T> void put(Class<T> key, List<T> value) {
map.put(key, value);
}

@SupressWarnings("unchecked")
public <T> List<T> get(Class<T> clazz) {
return (List<T>) map.get(clazz);
}
}
  • Wildcard ? in map declaration does not ensure that key Class<?> and value List<?> would be of the same type. Method put() ensures that. You can not declare map as Map<Class<T>, List<T>> if your class is not generic - that's why you have to use a method.

  • Get method is unchecked. The cast is safe if entries are added with put() method. (There's still a problem with raw types - but this is unavoidable)

  • You can add more methods to TypedMap class, but remember about this restrictions.



public static void main(String[] args) {
TypedMap map = new TypedMap();
List<Cat> cats = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();
adder.put(Cat.class, cats);
adder.put(Dog.class, dogs);

adder.put(Cat.class, dogs); // compilation error
}

Typescript array of different generics

The error you are seeing here is related to variance. The two functions format and sort use T in a contravariant position making the type TableColumnConfig contravariant which reverses assignability. You may also have other properties in TableColumnConfig in a covariant position making the type ultimately invariant.

You can bypass the error by using methods instead of properties containing functions. As I learned today, methods can be optional too.

export class TableColumnConfig<T> {

format?(val: T): string
sort?(val1: T, val2: T): number

constructor() {}
}

const columns: TableColumnConfig<unknown>[] = [];

// this works fine now
columns.push(new TableColumnConfig<number>());
columns.push(new TableColumnConfig<string>());

This works because variance works differently for methods. See here.

Method types both co-vary and contra-vary with their parameter types


Playground

Typescript array of different generic types

The element type of the array would need to be an "existential type", which we could write in pseudocode as exists T. { first: T, second: (arg: T) => string }. TypeScript currently does not support existential types natively.

One potential workaround is to encode existential types using closures as explained in this answer. If you don't want to use real closures at runtime, you can use a utility library that provides a type definition for existential types (based on an encoding of type functions using indexed access types) and functions to produce and consume existential types that perform type casts but are just the identity at runtime:

// Library
// (Based in part on https://bitbucket.org/espalier-spreadsheet/espalier/src/b9fef3fd739d42cacd479e50f20cb4ab7078d534/src/lib/type-funcs.ts?at=master&fileviewer=file-view-default#type-funcs.ts-23
// with inspiration from https://github.com/gcanti/fp-ts/blob/master/HKT.md)

const INVARIANT_MARKER = Symbol();
type Invariant<T> = {
[INVARIANT_MARKER](t: T): T
};

interface TypeFuncs<C, X> {}

const FUN_MARKER = Symbol();
type Fun<K extends keyof TypeFuncs<{}, {}>, C> = Invariant<[typeof FUN_MARKER, K, C]>;

const BAD_APP_MARKER = Symbol();
type BadApp<F, X> = Invariant<[typeof BAD_APP_MARKER, F, X]>;
type App<F, X> = [F] extends [Fun<infer K, infer C>] ? TypeFuncs<C, X>[K] : BadApp<F, X>;

const EX_MARKER = Symbol();
type Ex<F> = Invariant<[typeof EX_MARKER, F]>;
function makeEx<F, X>(val: App<F, X>): Ex<F> {
return <any>val;
}
function enterEx<F, R>(exVal: Ex<F>, cb: <X>(val: App<F, X>) => R): R {
return cb(<any>exVal);
}

// Use case

const F_FirstAndSecond = Symbol();
type F_FirstAndSecond = Fun<typeof F_FirstAndSecond, never>;
interface TypeFuncs<C, X> {
[F_FirstAndSecond]: { first: X, second: (arg: X) => string };
}

let myArray: Ex<F_FirstAndSecond>[];
myArray.push(makeEx<F_FirstAndSecond, number>({ first: 42, second: (x) => x.toString(10) }));
myArray.push(makeEx<F_FirstAndSecond, {x: string}>({ first: {x: "hi"}, second: (x) => x.x }));
for (let el of myArray) {
enterEx(el, (val) => console.log(val.second(val.first)));
}

(If there's enough interest, I may properly publish this library...)



Related Topics



Leave a reply



Submit