Why does a TypeScript generic suppress type errors?

Viewed 54

Take the following TypeScript example.

function expectInterface(args: { foo: string }): void {}

function passthrough<T>(fields: T): T {
  return fields
}

expectInterface({
  foo: 'bar',
  badKey: 'baz', // error
})

expectInterface(passthrough({
  foo: 'bar',
  badKey: 'baz', // no error
}))

Why does passthrough cause the type error on badKey to get suppressed?

1 Answers

The error on badKey is suppressed because TypeScript uses a structural typing system. This means that one type X is compatible with another type Y if type X contains at least all the fields which type Y has. See the type compatibility section in the TypeScript docs for more specific details.

For clarity, let's name your interfaces HasFoo and HasFooAndBadKey.

interface HasFoo {
  foo: string;
}

interface HasFooAndBadKey {
  foo: string;
  badKey: string;
}

You can see that HasFooAndBadKey contains the field foo, which means it is compatible with type HasFoo. Therefore this code is valid and compiles just fine:

const withBadKey: HasFooAndBadKey = {
  foo: "bar",
  badKey: "baz"
};

const item: HasFoo = withBadKey;

However, assigning the same way using a literal will fail with the error Object literal may only specify known properties, and 'badKey' does not exist in type 'HasFoo'.

const other: HasFoo = {
  foo: "bar",
  badKey: "baz" // Error
}

Therefore your error isn't caused by generics. Your error is happening because you are only allowed to specify known properties in literals. Your generic passthrough function does not result in a compile time error because in this case it is returning a non-literal value which is type compatible with HasFoo. Explicitly typing passthrough to take a HasFoo and passing it the same literal containing badKey will result in the same error.

interface HasFoo {
  foo: string;
}
interface HasFooAndBadKey {
  foo: string;
  badKey: string;
}

function expectInterface(args: HasFoo): void {}

function passthrough<T>(fields: T): T {
  return fields
}

// Expecting literal HasFoo, which can only have a foo field
expectInterface({
  foo: 'bar',
  badKey: 'baz', // error
})

const item: HasFooAndBadKey = {
  foo: 'bar',
  badKey: 'baz'
};

expectInterface(item); // No error

// Non-literal value returned by passthrough. The non-literal
// is type compatible with HasFoo, so no error occurs.
expectInterface(passthrough<HasFooAndBadKey>({
  foo: 'bar',
  badKey: 'baz', // no error
}))

// Now passthrough shows an error becuase we told it to expect a HasFoo,
// and this HasFoo literal cannot contain any other key than 'foo'.
expectInterface(passthrough<HasFoo>({
  foo: 'bar',
  badKey: 'baz', // error
}))
Related