1

I have a problem with generics of get function.

Take a look at functions main1 and main1a

If I save the result of get function into variable - typescript says that arg = arg || fooBar; type is FooBar

But if I just go for arg = arg || get(FooBar); typescript says, that the type is FooBar | undefined

export interface IType<T = any> extends Function {
  new (...args: any[]): T;
}

interface IFooBar {}
class FooBar implements IFooBar {}

function get<TInput = any, TResult = TInput>(someClass: IType<TInput>): TResult {
  // stub
  return someClass as any;
}

class Baz {
  constructor(public foo: IFooBar) {
    //
  }
}

export function main1(arg?: FooBar) {
  const fooBar = get(FooBar);
  arg = arg || fooBar;
  // works as expected
  // no error: arg type is FooBar
  return new Baz(arg);
}

export function main1a(arg?: FooBar) {
  arg = arg || get(FooBar);
  // unexpected error, why?
  // error: FooBar | undefined
  return new Baz(arg);
}

export function main2(arg?: FooBar) {
  // works as expected
  // error: FooBar | undefined
  return new Baz(arg);
}

1 Answer 1

1

IN order to make it work, you should rewrite your get function. It should be as follow:

function get<TInput>(someClass: IType<TInput>) {
  // stub
  return someClass;
}

Why ? This code is coompletely unsafe:

export interface IType<T = any> extends Function {
  new(...args: any[]): T;
}

interface IFooBar { }

class FooBar implements IFooBar {
  name = 'hello'
}

class Custom {
  age: number = 42
}

function get<TInput = any, TResult = TInput>(someClass: IType<TInput>): TResult {
  // stub
  return someClass as any
}


const result = get<{}, Custom>(FooBar)
result.age // TS think it is number, but it is undefined

Playground

TypeScript allows you to get age property whereas it should be disallowed.

Working code:

export interface IType<T = any> extends Function {
  new(...args: any[]): T;
}

interface IFooBar { }

class FooBar implements IFooBar {}


function get<TInput>(someClass: IType<TInput>) {
  // stub
  return someClass
}

class Baz {
  constructor(public foo: IFooBar) {
    
  }
}

export function main1(arg?: FooBar) {
  const fooBar = get(FooBar);
  arg = arg || fooBar;
  return new Baz(arg); // ok
}

export function main1a(arg?: FooBar) {
  arg = arg || get(FooBar);

  return new Baz(arg); // ok
}

export function main2(arg?: FooBar) {
  return new Baz(arg); // expected error
}

Playground

Try to avoid using generics as an explicit return types. It does not work in a way you expect in 80% of cases. I'm not saying it always does not work, I'm just saying that from my experience - it is a very common error that developers are struggling with.

Let TypeScript do his job, it can infer 95% of return types without helping.

From my experience, you should use explicit return types when you are using recursion.

If you still want to use some generic as a return type, try to overload your function (docs). With overloading, you don't need to use as any type assertion. However there is a drawback.

Remember that I said it is unsafe? So it is true. Function overloads are bivariant. So it is up to you.


I still wonder why are const foobar = get(FooBar) and arg || get(FooBar) has different behaviours

This is your code example:

export function main1a(arg?: FooBar) {
  arg = arg || get(FooBar);
  return new Baz(arg);
}

Seems that arg variable some how affects second generic argument of get function. Hover your mouse over get(FooBar), you will see function get<FooBar, FooBar | undefined>(someClass: IType<FooBar>): FooBar | undefined. Try to write this expression:42 || get(FooBar) , you will get function get<FooBar, 42>(someClass: IType<FooBar>): 42.

Honestly, I'm unable to explain this behavior.

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

4 Comments

In your example you explicitly put types into generic. const result = get<{}, Custom>(FooBar). Ik it should not work like that if you are not letting typescript decide which type is he using now. You code will endup with an error if you remove explicit types.
Yes, you are 100% right. I should not do it, and in 90% cases you should not write explicit generic parameters by your self, however TS allows me to do it. It means that other developers will be allowed to do it.
I still wonder why do const foobar = get(FooBar) and arg || get(FooBar) has different behaviours.
@Puwka made an update, unfortunatelly, I'm unable to understand this behavior

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.