7

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?

TN.
  • 18,874
  • 30
  • 99
  • 157

3 Answers3

2

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

bela53
  • 3,040
  • 12
  • 27
  • I cannot change the code, because the real code is in a third-party library (react). (This is just heavily simplified example for demonstration purposes.) Could you provide some references or issues where the behavior is specified / tracked? – TN. Nov 11 '20 at 18:00
  • https://github.com/microsoft/TypeScript/issues/28884 is one example of generics not being interpreted further. https://github.com/microsoft/TypeScript/issues/13995 another more prominent one (latter focused on union narrowing). – bela53 Nov 11 '20 at 21:00
  • 1
    That you for the issue links! – TN. Nov 12 '20 at 09:52
1

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
}
pzaenger
  • 11,381
  • 3
  • 45
  • 46
  • I was trying to read the link, but `5xx Oops! Something happened to Medium.`. – TN. Nov 11 '20 at 16:15
  • Retry, I seems to me that `does not understand the subtle type narrowing here and throws a compile-time error` in the post refers something different: `filter` does not return different type. – TN. Nov 11 '20 at 16:20
  • It's pretty hard to find a comprehensible explanation for the described behavior (at least for me). I keep looking. – pzaenger Nov 11 '20 at 16:25
0

This seems to be a limitation of type aliases.

interface Foo  { name: string }

function fooWorks<T extends Foo>(input?: T) {
    return input?.name // ok
}




function fooErrors<T extends Foo>(input?: Readonly<T>) {
    return input?.name // error
}

Behaves as expected.

Neal
  • 573
  • 3
  • 16
  • Unfortunately `{ name?: string }` != `{ name: string } | undefined`. Is it known issue? – TN. Nov 11 '20 at 14:17
  • `fooErrors(input: Readonly)` does not work. – TN. Nov 11 '20 at 14:19
  • Sorry, I misread the type slightly. I have updated it. Known limitations are well outlined in this question: https://stackoverflow.com/questions/37233735/typescript-interfaces-vs-types – Neal Nov 11 '20 at 15:12