0

I am currently reading up on Generic Comparison's in TypeScript. I get it that types are removed when transpiling from TypeScript to JavaScript so you can't compare two generic types.

So this is a 3-part question. Question 1: Can someone explain what extends Array does here? Question 2: I know when I console.log(c3), I get [ '1', '2', '500', '600' ] So when I call concatenate(l1, l4), because l1 and l4 share the same ID and type, const oneList is created and then returned? Question 3: What is T1 doing? Is it essentially the ID being used for list1 and list2?

class IdentificatedGeneric<S> extends Array<S> {
    public id: string; // Enhancement of Array class
    public constructor(id: string) {
        super();
        this.id = id;
    }
}
function concatenate<S, T1 extends IdentificatedGeneric<S>>(list1: T1, list2: T1): T1 {
    if (list1.id === list2.id) { // Comparison to ensure from the same id, possible because both extends IdentificatedGeneric
        const oneList = [...list1, ...list2] as T1;
        return oneList;
    }
    throw Error("Must be the same id");
}

const l1 = new IdentificatedGeneric<string>("l1");
const l2 = new IdentificatedGeneric<string>("l2");
const l3 = new IdentificatedGeneric<number>("l1");
const l4 = new IdentificatedGeneric<string>("l1");

l1.push("1", "2");
l2.push("100", "200");
l3.push(5, 6);
l4.push("500", "600");

const c3 = concatenate(l1, l4);
console.log(c3);
3
  • 1
    Hmm, where did you find this code? The signature for concatenate() is bad; it falsely claims to return some subtype of IdentificatedGeneric when in reality it is returning a plain old array without an id property. The compiler will let you write c3.id.toUpperCase() without warning even though it blows up at runtime. Anyway this means that Q2 and Q3 have "I don't know what's going on here but it's not right" as the answer. Commented Oct 26, 2020 at 0:52
  • 1
    @jcalz This was on a website educative.io/courses/learn-typescript-complete-course/…. This is a course I am taking, Commented Oct 26, 2020 at 1:01
  • 1
    heh, uh oh. That's unfortunate Commented Oct 26, 2020 at 1:05

1 Answer 1

1

Q1: The extends in class Foo extends Bar is part of JavaScript (well, ECMAScript 2015 and above) and not only part of TypeScript's static type system. Therefore it does not get erased. If the target JavaScript version is ES2015 or later, the emitted JavaScript code will contain class Foo extends Bar directly. If it is something lower, like ES5, it will be transpiled to code which works the same (ish), but will still be included. Both Foo and Bar constructors will exist in the emitted code.

Anyway, class Foo extends Bar is called subclassing, and it produces a class constructor Foo whose instances will inherit behavior from the superclass Bar. Also, new Foo() instanceof Bar will be true (all Foo instances are also Bar instances), but new Bar() instanceof Foo() will be false (not all Bar instances are also Foo instances).

When the superclass is Array as in class ABC<T> extends Array<T> {}, it means that you are extending a built-in class. Every instance of your subclass should also be an instanceof Array.


Now the rest of your question is a bit confusing because the code for concatenate() is... well, it's not good. I will answer what the type signature says it does, and note how it differs from what the implementation actually does:

declare function concatenate<S, T1 extends IdentificatedGeneric<S>>(
  list1: T1, list2: T1
): T1;

This signature says that concatenate() takes two arguments, both of which are some subtype T1 of IdentificatedGeneric<S> for some type S. (Since S is not mentioned directly in either the list1 or list2 parameter type, the compiler has no clue how to infer it and will likely infer unknown for it. So let's ignore S for now). And the return value of concatenate() will be the same T1 type as each of its inputs. So if the inputs are both, for example, IdentificatedGeneric<string>, then the return type will also be IdentificatedGeneric<string>. Meaning it has an id property of type string.

Note from the implementation that this is not true:

  if (list1.id === list2.id) { 
    const oneList = [...list1, ...list2] as T1; // 🤥
    return oneList;
  }
  throw Error("Must be the same id");

That oneList is absolutely not an IdentificatedGeneric of any sort. It's just a regular Array because it was made as an array literal. Remember, while all IdentificatedGeneric instances are instanceof Array, not all Array instances are instanceof IdentificatedGeneric.

And this is why the implementation is using a type assertion and saying as T1. If you leave that off, the compiler will rightfully complains:

const oneList = [...list1, ...list2];
return oneList; // error!
// Type 'T1[number][]' is not assignable to type 'T1'.

the compiler is saying "wait, you said you're returning a T1, but you actually just took elements from a T1 and put them into a plain array. That's not the same". Whoever implemented this used a type assertion as T1 to just tell the compiler "trust me, it's the same". But this is a lie! 🤥 Oh well.


So, the rest of your questions:

Q2: checking that id is the same only really happens at runtime, while checking that the type parameter in IdentificatedGeneric<T> is the same only really happens at compile time. They are unrelated.

So when you call concatenate(l1, l4), this does not give a compiler error because both are string arrays, but this has nothing to do with what happens at runtime. And it does not give a runtime error because both ids are the same, but this has nothing to do with the code compiling without warning.

If you call concatenate(l1, l3), you will get a compiler warning, but the emitted JavaScript will not throw an error and oneList will still be created if you run the code. On the other hand, if you call concatenate(l1,l2), the compiler will be happy with it, but there will be an error at runtime because the id field is not the same.


Q3: T1 is the specific identical subtype of IdentificatedGeneric that is passed to concatenate(). If you subclass IdentificatedGeneric you can get T1 to be inferred as that subclass. This happens if you pass two instances of the subclass into concatenate()... and the signature says the output will be of the same subclass type:

class Sub1 extends IdentificatedGeneric<string> {
  prop1 = "hello"
}
concatenate(new Sub1("l1"), new Sub1("l1")).prop1.toUpperCase();
// here T1 is inferred to be Sub1

As I said before though, the implementation does no such thing.


As an aside, here's one way I might improve concatenate():

function concatenate2<S>(
  list1: IdentificatedGeneric<S>, 
  list2: IdentificatedGeneric<S>
): IdentificatedGeneric<S> {
  if (list1.id === list2.id) {
    const concatList = new IdentificatedGeneric<S>(list1.id);
    concatList.push(...list1, ...list2);
    return concatList;
  }
  throw Error("Must be the same id");
}

See how the implementation is actually returning a real IdentificatedGeneric? It creates a new one and fills it with elements.

Also, I am no longer claiming that concatenate() can return any possible subtype of IdentificatedGeneric. Instead, if you pass in two IdentificatedGeneric<S> for the same type S, it will return another IdentificatedGeneric<S>. This signature is only generic in S and you do not have to worry about dragging possible subtypes around. for example, the following code is now fine:

const c2 = concatenate2(new Sub1("l1"), new Sub1("l2")); // IdentificatedGeneric<string>

Here c2 is no longer considered to be a Sub1 by the compiler, but just an IdentificatedGeneric<string>... which it is.


Playground link to code

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.