Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript generics does not work with Readonly

Tags:

typescript

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?

like image 376
TN. Avatar asked Nov 11 '20 13:11

TN.


People also ask

Does TypeScript support generics?

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.

How do generics work in TypeScript?

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.

How do I use generic classes in TypeScript?

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> .

What are some properties of generics in TypeScript?

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.


2 Answers

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

like image 59
bela53 Avatar answered Nov 15 '22 08:11

bela53


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
}
like image 30
pzaenger Avatar answered Nov 15 '22 07:11

pzaenger