Home > Software design >  TypeScript type inference from default parameter of type
TypeScript type inference from default parameter of type

Time:11-10

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.

CodePudding user response:

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

CodePudding user response:

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;
};

Although your assign the type Foo to obj it will also work for getName(bar); because Bar extends Foo. In other words: Since TypeScript knows that Foo is more specific than Bar any element of type Bar will be accepted by the getName function.

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)

CodePudding user response:

T extends Foo means that T is a subtype of Foo. It means that T has all properties of Foo but also might have infinity amount of extra properties.

Consider this example:

interface Foo {
  name: string;
}

interface Bar extends Foo {
  displayName: string;
}

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

getName<Bar>(); // no error

So, what we have? getName is called with Bar generic parameter, hence obj argument should have Bar type. Right? But according to your example, obj has default value foo. It is unsafe behavior.

This is why TS disallows you to use T = foo

IN your case, do don't need generic at all.

interface Foo {
  name: string;
}

interface Bar extends Foo {
  displayName: string;
}

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

function getName(obj: Foo = foo) {
  return obj.name;
};

getName(); // ok

getName(bar); // ok

Playground

TS allows you to call getName with bar argument since bar extends Foo.

From the other hand, you are not allowed to use literal object as an argument:

getName({ name: "bar", displayName: "dn" }); // ok

Because of excess-property-checks

If you still want to use generic T you need to go with overloading:

interface Foo {
  name: string;
}

interface Bar extends Foo {
  displayName: string;
}

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

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

getName(); // ok

getName({ name: "bar", displayName: "dn" }); // ok
  • Related