0

Given the following code, I expect it to compile without errors:

type Immutable<T> = T extends object 
    ? { readonly [K in keyof T]: Immutable<T[K]>; }
    : T;

function foo<T>(t: T): Immutable<T> {
    return t;
}

Open in playground

However tsc outputs a quite unhelpful error message:

Type 'T' is not assignable to type 'Immutable<T>'.

The error disappears when the conditional is removed:

- type Immutable<T> = T extends object 
-    ? { readonly [K in keyof T]: Immutable<T[K]>; }
-    : T;
+ type Immutable<T> = { readonly [K in keyof T]: Immutable<T[K]>; };

function foo<T> (t: T): Immutable<T> {
    return t;
}

Why does the original error occur? Why the conditional type here breaks the variance T extends Immutable<T>? I want to have a solution that won't use as T casts whatsoever.

Meta:

$ npx tsc --version
Version 4.1.3

Unfortunately, changing the definition of Immutable is complicated (it requires a PR to the upstream repo).

The original problem was when using immer library (here is the definition if Immutable<T>), and as far as I can tell, the authors of immer haven't found the solution and thus propose a workaround with castImmutable<T>(): Immutable<T> function

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102
Veetaha
  • 833
  • 1
  • 8
  • 19

1 Answers1

1

This approach:

type Immutable<T> = T extends object
    ? { readonly [K in keyof T]: Immutable<T[K]>; }
    : T;

means that if Immutable receives an object it will freeze it, other wise it will return type as is.

Your foo function does not have such condition, that's why TS complains. TS can't figure out when it should return T.

To help TS, you can provide overloadings:

type Immutable<T> = T extends object
    ? { readonly [K in keyof T]: Immutable<T[K]>; }
    : T;


function foo<T extends object>(t: T): { readonly [K in keyof T]: Immutable<T[K]>; }
function foo<T>(t: T): { readonly [K in keyof T]: T }
function foo<T>(t: T): Immutable<T> | T {
    if (typeof t === 'object') {
        return Object.freeze(t) as Immutable<T>
    } else {
        return t
    }
}

const result1 = foo({ age: 42 }) // { readonly age: number; }
const result2 = foo(42) // number

Now, TS understand, that when foo receives primitive value, it should return T