Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding Generic Constraint Extends in TypeScript

I'm trying to create a generic function that accepts sub-types of a base class, and returns a promise that resolves to an instance of the specified class. This example shows what I'm trying to achieve:

class foo {}
class bar extends foo {}
const someBar = new bar();
function foobar<T extends foo>(): Promise<T> {
  return new Promise<T>(resolve => resolve(someBar)); // this doesn't compile
}

I realize that TypeScript is using structural typing, so it's going to accept any type for T in this trivialized example. But I'm not sure why it won't let me return a value of someBar.

Is there a way to achieve what I'm trying to do here? Thanks!

The compiler error I'm getting is:

const someBar: bar
Argument of type 'bar' is not assignable to parameter of type 'T | PromiseLike<T> | undefined'.
  Property 'then' is missing in type 'bar' but required in type 'PromiseLike<T>'.ts(2345)
lib.es5.d.ts(1393, 5): 'then' is declared here.

Update

Per request, here's some additional information about what I'm trying to accomplish. This code compiles fine (I've added functions to foo and bar, just to structurally differentiate them):

class foo {
  f() {}
}
class bar extends foo {
  b() {}
}
const someBar = new bar();
function foobar(): Promise<foo> {
  return new Promise<foo>(resolve => resolve(someBar));
}
foobar().then(result => {
  const myBar: bar = result as bar;
  console.log(myBar);
});

I was trying to avoid the need to downcast the polymorphic result of the promise- const myBar: bar = result as bar, as shown below:

class foo {
  f() {}
}
class bar extends foo {
  b() {}
}
const someBar = new bar();
function foobar<T extends foo>(): Promise<T> {
  return new Promise<T>(resolve => resolve(someBar));
}
foobar<bar>().then(result => {
  const myBar: bar = result;
  console.log(myBar);
});

TS is properly inferring that result is a bar, but it's not letting me return someBar in the function.

I've been able to achieve this using the polymorphic this in classes where my generic class method was dealing with this - but here I'm not in a class.

Update 2

Actually, this doesn't need the promise to illustrate what I'm doing. Here I've simplified further (and left out the definitions of foo and bar, since that isn't changing):

function foobar<T extends Foo>(): T {
  return someBar;
}
const mybar: Bar = foobar<Bar>();

And, here's what I'm doing in 'pure javascript':

var someBar = new bar();
function foobar() {
  return someBar;
}
var myBar = foobar();

As you can see, what I'm doing is trivial. I'm just trying to get polymorphic type checking without downcasting.

like image 934
Eric S Avatar asked Jan 30 '26 06:01

Eric S


1 Answers

The code you've provided is not a generic function, so you're going down the wrong path trying to type it as one.

A generic function is one where it needs to work with a variety of data types, while still being able to specify the relationship between types in different parts of the function. For example, you can specify that the function gets passed some type, and then returns an array of that same type

function wrapInArray<T> (input: T): T[] {
   return [T];
}
// To be used like:

const result1 = wrapInArray(1234); // result1 is a number array;
const result2 = wrapInArray('hello'); // result2 is a string array;

In the above example, T is a placeholder for whatever type happens to be passed in. It's purpose is to tell typescript how things relate to eachother. You may not know ahead of time what T is, but you can still say that the inputs and the outputs are related to eachother.


In many cases, you want to be a bit more specific with your generics. This is because if you're not specific, then you can't do much inside the function, because you don't know what types you're working with. By using extends, you can specify that the type has to have certain properties. This will forbid anyone from calling your function with anything that doesn't have those properties, and then will allow you to use those properties at will inside the function. For example:

function getLength<T extend { length: number }>(input: T): number {
  // I can access input.length because even though i don't know what input's type is,
  //   i do know that it's illegal to call this function with anything that doesn't
  //   have a length property.
  return input.length;
}

// to be used like:

const len1 = getLength([1, 2, 3]);
const len2 = getLength({ length: 5 });

Your function only ever does one thing: returns a Promise<bar>. There are no associations between types that you need to specify, and there is no possible way to call the function which would cause it to return something other than a Promise<bar>. So the correct types to have are:

const someBar = new bar();
function foobar(): Promise<bar> {
  return new Promise(resolve => resolve(someBar));
}

Update:

function foobar<T extends Foo>(): T {
  return someBar;
}
const mybar: Bar = foobar<Bar>();

Again, this code is not an example of a generic function. It always returns a bar, never anything else. It should be typed like this:

function foobar(): Bar {
  return someBar;
}


const myBar = foobar(); 
// You could add the type explicitly if you prefer, but it's not necessary:
// const myBar: Bar = foobar();
// And since anything that's a Bar necessarily has all the properties of a Foo, you can also do:
const myFoo: Foo = foobar();

like image 141
Nicholas Tower Avatar answered Feb 01 '26 20:02

Nicholas Tower