Let's have the following simplified example in TypeScript:
type Foo = { name: string } | undefined
function fooWorks<T extends Foo>(input: T) {
return input?.name // ok
}
function fooErrors<T extends Foo>(input: Readonly<T>) {
return input?.name // error
}
Why fooWorks
works, but fooErrors
does not?
TypeScript fully supports generics as a way to introduce type-safety into components that accept arguments and return values whose type will be indeterminate until they are consumed later in your code.
The TypeScript documentation explains Generics as “being able to create a component that can work over a variety of types rather than a single one.” Great! That gives us a basic idea. We are going to use Generics to create some kind of reusable component that can work for a variety of types.
TypeScript supports generic classes. The generic type parameter is specified in angle brackets after the name of the class. A generic class can have generic fields (member variables) or methods. In the above example, we created a generic class named KeyValuePair with a type variable in the angle brackets <T, U> .
TypeScript Generics is a tool which provides a way to create reusable components. It creates a component that can work with a variety of data types rather than a single data type. It allows users to consume these components and use their own types.
The following will preserve the Readonly
type and undefined
as desired:
function fooErrors<T extends Readonly<Foo>>(input: T) {
return input?.name // works
}
Note: we set Readonly
in the generic constraint and don't wrap it around the parameter type.
The reason, why fooErrors<T extends Foo>(input: Readonly<T>)
causes issues is: TypeScript does not process unresolved generic type parameters (like T
) further. Readonly
essentially is a mapped type. So in the function body, input
has type { readonly [P in keyof T]: T[P]; }
, which unfortunately is not assignable back to T
and its constraint Foo
. From the compiler's perspective property name
on the parameter cannot be found anymore.
Playground code
It gives the following error:
Property 'name' does not exist on type
NonNullable
.
The problem is, that the TypeScript compiler does not understand the subtle type narrowing here and throws a compile-time error as stated here (though, I am not 100% sure about how helpful this blog post is for you).
Removing undefined
from Foo
works:
type Foo = { name: string }
function fooWorks<T extends Foo>(input: T) {
return input?.name
}
function fooErrors<T extends Foo>(input: Readonly<T>) {
return input?.name
}
Or you can add the NonNullable
type excluding null
and undefined
from T
, which results in input
being { name: string }
:
type Foo = { name: string } | undefined
function fooWorks<T extends Foo>(input: T) {
return input?.name
}
function fooErrors<T extends Foo>(input: Readonly<NonNullable<T>>) {
return input.name
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With