1

Given the following example:

interface Foo {
  name: string;
}

interface Bar extends Foo {
  displayName: string;
}

const foo: Foo = { name: "foo" };
const bar: Bar = { name: "bar", displayName: "dn" };

const getName = <T extends Foo>(obj: T = foo): string => {
  return obj.name;
};

getName(foo); // Ok
getName(bar); // Ok

obj: T = foo causes an error. Even though, T extends Foo, this is not accepted. Why is this the case and what would be an workaround?

Example.

Vivere
  • 1,919
  • 4
  • 16
  • 35

2 Answers2

3

The problem is that foo is not of type T, suppose if you want to instantiate the generic with the interface Bar:

const getName = (obj: Bar = foo): string => {
  return obj.name;
};

You can see that foo is not a Bar (is missing the displayName property) and you can't assign it to a variable of type Bar.

You have now three options:

  1. Force foo to be used as a generic T: <T extends Foo>(obj: T = foo as T): string => .... But this is the worst choiche you could do (could lead to unexpected results/errors).

  2. Overloading your function to match your generic specifications. This is useful if you need to return (and actually use) your generic instance:

const getName: {
  // when you specify a generic on the function
  // you will use this overload
  <T extends Foo>(obj: T): T;

  // when you omit the generic this will be the overload
  // chosen by the compiler (with an optional parameter)
  (obj?: Foo): Foo;
}
= (obj: Foo = foo): string => ...;
  1. Notice that your function does not use at all the generic and pass the parameter as a simple Foo: (obj: Foo = foo): string => .... This should be the best one in your case
DDomen
  • 1,808
  • 1
  • 7
  • 17
1

You don't need a Generic for your getName function and can use a type annotation of Foo for the input parameter obj, i.e.:

const getName = (obj: Foo = foo): string => {
  return obj.name;
};

This is because in Typescript every complex type (e.g. objects and arrays) is covariant in its members.

In other words: Since TypeScript knows that Foo here has less required fields and therefore Bar is a subtype of Foo any element of type Bar will be accepted by the getName function. That is, you can safely use a Bar anywhere a Foo is required. (As noted by @DDomen in the comment, function parameter types behave differently)

Let me explain covariance:

  • Although you assign the type Foo to obj it will also work for getName(bar); because Bar extends Foo (and thereby Bar is a subtype of Foo).

  • This is allowed because of the way the Typescript engineers set up the type system. They decided that members of objects (i.e. complex types like shapes and arrays) should be allowed to receive its defined type and anything which is a subtype of that type.

  • In type theory this behavior can be described as shapes being covariant in its members.

  • Other type systems don't allow such flexibility, i.e. to use a subtype in such a situation. Such type systems would then by called invariant on members of a shape because they'd require exactly the type Foo and would not allow for this flexibility which TypeScript allows.

  • Also note that Bar does not have to explicitly extend Foo because TypeScript is structurally typed. TypeScript compares the structure of two objects to see whether one is the sub- or supertype of the other. So instead of interface Bar extends Foo you could also just define Bar as:

    interface Bar {
      name: string;
      displayName: string;
    }
    

    and getName will still be properly typed with its parameter obj as Foo.

When to use generics?

You don't need a generic because you don't want to relate different parts of your function to each other. A valid use of a generic may be if you want to relate your input parameter to your output parameter.

See this TS Playground of your code.

See also this related issue concerning the error you're getting, i.e.

Type 'Foo' is not assignable to type 'T'.
  'Foo' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Foo'.(2322)
Andru
  • 5,954
  • 3
  • 39
  • 56
  • The link points to an incomplete file – Vivere Nov 09 '21 at 09:44
  • @Vivere Sorry, fixed the link – Andru Nov 09 '21 at 09:49
  • Care, typescript is not *covariant* itself, but all the user defined types in typescript are (variance is a property of the type, not of the language). Also here you have one of the few cases of *contravariance* in typescript, because `getName` is a function. Indeed, resuming our example, you could assign a function with signature `(obj: Foo) => '1' | '2'` to a variable of signature `(obj: Bar) => string`, which match the definition of contravariance: A type `T` is *contravariant* if having `S <: P`, then `T

    <: T` (`Bar <: Foo` then `Function <: Function`, `<:` means subtype)

    – DDomen Nov 21 '21 at 17:52
  • @DDomen Thanks, I changed the formulation of the first sentence. Indeed, function types are contravariant in its parameter types in TS. However, if I have a function `getName` with type `(obj: Foo = foo): string` it will accept an object of type `Bar` as its argument if `Bar <: Foo` (where `<:` means subtype). Isn't this (i.e. the fact that I can provide a value of type `Bar` where a value of its supertype `Foo` is expected) the definition of covariance or am I mislead? I just recently learned about variance. I very much appreciate your thorough comment! – Andru Nov 23 '21 at 18:55
  • @Andru sorry for the late, busy period. Your correction is right. What you are mentioning is subtyping, where you can assign an object of type `Bar` to a variable of type `Foo` (given `Bar <: Foo`). Variance describes subtyping for generic types, so ansewer the question "Which is the relation between `MyType` and `MyType`?". In TS all the user defined types are **covariant**, so `MyType <: MyType`, only exceptions are functions, which are **contravariant** (`Func <: Func`). It means you can do `var f: (p: Bar) => ... = (p: Foo) => ...` but not `var f: Foo = Bar` – DDomen Nov 26 '21 at 22:21
  • @DDomen Thanks again. Should I adapt my answer and scrape my description of covariance because it is not relevant here? In the book “Programming TypeScript” (from Boris Cherny) I read on page 117: “When talking about types, we say that TypeScript shapes (objects and classes) are covariant in their property types.” - This made me believe that variance is a general way to describe subtype and supertype relationships in type theory and can therefore not only be used for parametrized (generic) types, but also as an explanation of why the type I suggest here works?! – Andru Nov 27 '21 at 18:59
  • @Andru no it is ok to talk of covariance here, because typescript uses structural subtyping (it checks the members of a structure instead of its raw type). What Boris Cherny is saying is related to structural typing, the covariance in this case is referred to the members of a type. Let's take `type Foo = { v1: number, v2: string }` as example, you can describe it as a parametrized type in non-structural subtyping fashion: `Foo = Object<[ [ 'v1', number ], [ 'v2', string ] ]>` (with `Object>` is the JS object type checker). Now you can apply the usual variance properties. – DDomen Nov 28 '21 at 17:50
  • Variance is an extension of subtyping, so its appliance really depends on the type system. Indeed, variance also affects multiple inheritance (in those languages which support it, like c++) and the method overriding behaviour (also in TS). In those languages that does not support these features, you can (almost always) achieve these by the mean of generics. So, at the end of all, we could refer to variance as a parametrized type property. Do you agree? – DDomen Nov 28 '21 at 18:06