Завдання

Реалізувати дженерик MyReadonly2<T, K>, що приймає два типи-аргументи T та K.

Тип K визначає множину властивостей з T, які мають стати readonly. Якщо K немає, він має зробити readonly всі властивості, як звичайний Readonly<T>. Наприклад:

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

Розв’язок

Це завдання є продовженням завдання Readonly<T>. Все майже таке ж, тільки треба додати новий тип-параметр K, щоб ми могли вказати, які конкретно властивості мають стати readonly.

Почнемо з найпростішого рішення. Розглянемо випадок, коли K є порожньою множиною, тож нам не потрібно робити щось readonly. Просто повертаємо T:

type MyReadonly2<T, K> = T;

Тепер потрібно врахувати випадок, коли K містить якісь властивості. Ми можемо використати оператор & і створити перетин двох типів: першим є наш тип T, а другим є множина властивостей, які треба зробити readonly:

type MyReadonly2<T, K> = T & { readonly [P in K]: T[P] };

Виглядає правдоподібно, та ми отримаємо помилку компіляції “Type ‘P’ cannot be used to index type ‘T’”. І це дійсно так, ми не встановили обмежень для K. Це має бути “кожен ключ з T”:

type MyReadonly2<T, K extends keyof T> = T & { readonly [P in K]: T[P] };

Працює? Ні! Ми не врахували випадок, коли K зовсім немає. Це той самий випадок, коли наш тип має поводитись так само як Readonly<T>. Щоб це виправити, ми просто вкажемо, що за замовчуванням K, це “всі ключі з T”:

type MyReadonly2<T, K extends keyof T = keyof T> = T & {
  readonly [P in K]: T[P];
};

Посилання