Readonly 2
Challenge
Implement a generic MyReadonly2<T, K>
which takes two type arguments T
and
K
.
K
specify the set of properties of T
that should set to readonly
. When K
is not provided, it should make all properties readonly
, just like the normal
Readonly<T>
. For example:
interface Todo {
title: string;
description: string;
completed: boolean;
}
const todo: MyReadonly2<Todo, "title" | "description"> = {
title: "Hey",
description: "foobar",
completed: false,
};
todo.title = "Hello"; // Error: cannot reassign a readonly property
todo.description = "barFoo"; // Error: cannot reassign a readonly property
todo.completed = true; // OK
Solution
This challenge is a continuation of Readonly<T>
challenge. Everything is pretty the same, except that we need to add a new type
parameter called K
so we could specify the exact properties to be read-only.
We start with the simplest solution, the case when K
is an empty set so that
nothing need to be read-only. We just return T
:
type MyReadonly2<T, K> = T;
Now, we need to handle the case, when we provide the properties in K
. We can
use &
operator and make
intersection of both types:
the one is the T
we had before and the second one is the type with read-only
properties:
type MyReadonly2<T, K> = T & { readonly [P in K]: T[P] };
Looks like a solution, but we are getting a compilation error “Type ‘P’ cannot
be used to index type ‘T’”. And that is true, we do not set a constraint on K
.
It should be “every key from T
”:
type MyReadonly2<T, K extends keyof T> = T & { readonly [P in K]: T[P] };
Works now? No! We do not handle the case when K
is not set at all. That is the
case when our type must behave as an usual Readonly<T>
type. To fix that, we
are just specifying the default type parameter for K
to be “all the keys from
T
”:
type MyReadonly2<T, K extends keyof T = keyof T> = T & {
readonly [P in K]: T[P];
};
The solution above does not work in TypeScript 4.5+, because the original behavior is a bug in TypeScript, filed at microsoft/TypeScript#45122 and fixed at microsoft/TypeScript#45263.
Intersections conceptually mean “and”, so {readonly a: string} & {a: string}
should be equivalent to {a: string}
, i.e., the property a
is writable and
readable. Before TypeScript 4.5, TypeScript had the opposite and incorrect
behavior, where a final object property is readonly
if it is readonly
in
some intersection members. So this is the reason the solution above does not
work.
To fix this, we omit the keys:
type MyReadonly2<T, K extends keyof T = keyof T> = Omit<T, K> & {
readonly [P in K]: T[P];
};
Comments