0

I was playing around with generics in typescript v4.0.2 and came across this oddity. Why does the compiler complain that T is not a string after I have narrowed it as such?

function foo<T>(arg: T): T {
    if (typeof arg === "string") {
        return "hello"
    }

    return arg
}

The error is:

Type 'string' is not assignable to type 'T'.
  'T' could be instantiated with an arbitrary type which could be unrelated to 'string'.
4
  • 1
    What if you call it as foo<"bar">("bar")? Commented Oct 2, 2020 at 16:55
  • 1
    Or more likely, const bar = "bar"; foo(bar); Commented Oct 2, 2020 at 16:57
  • I have looked at the typescript handbook and there doesn't see to be an easy way to do what I am trying to do? Because from a JS perspective "bar" is only of type string Commented Oct 2, 2020 at 17:03
  • TS types are a lot more detailed than what JavaScript's typeof x will give you at runtime, so I'm not sure that ""bar" is only a type of string" is a fruitful line of reasoning. If what you are trying to do is return string when T extends string and T otherwise, then you will end up needing either conditional types or overloads, each of which has drawbacks. I wonder what your use cases are though. Commented Oct 2, 2020 at 17:08

1 Answer 1

3

This function signature:

foo<T>(arg: T): T

Returns the same type as its argument. This means if you pass in the string "Sebastian", you expect to get "Sebastian" back, not just any string. But the way you have written this function, it would return "hello" instead, which has a different string literal type.

To support this logic, I think function overloads are what you want. You would create a special call signature just for strings that returns strings, then a generic call signature that handles all other argument types.

function foo(arg: string): "hello"
function foo<T>(arg: T): T
function foo<T>(arg: T): T | "hello" {
    if (typeof arg === "string") {
        return "hello"
    }

    return arg
}

const a = foo('testing')     // type: "hello"
const b = foo(123)           // type: 123
const c = foo(123 as number) // type: number
const d = foo({ abc: 123 })  // type: { abc: 123 }

Playground

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

3 Comments

If the argument is a union including a string literal it will give a badly typed result. e.g., foo(Math.random() < 0.5 ? "bar" : 2) returns "bar" | 2 when you probably want "hello" | 2. Personally I'd have just one call signature, foo<T>(arg: T): T extends string ? "hello" : T;. You can still use the foo<T>(arg: T): T | "hello" implementation signature, though. Like this
Thanks, it is definitely the overloading that I wanted. Though more specifically I went with the following: function foo(arg: string): string; function foo<T>(arg: T): T;
@jcalz Indeed. I was thinking that mixing an unconstrained generic in one overload where the other overload had no generic might get a bit weird.

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.