typescriptnestedreadonlyreadonly-attribute

Make all fields in an interface readonly in a nested way


I know that, we can use Readonly< T> to redeclare all fields of T as readonly, as in: [Typescript: extending an interface and redeclaring the existing fields as readonly

What about nested fields? For example:

interface School {
  teachers: Array<Teacher>;
  students: Array<Student>;
}

interface Teacher {
  teacherId: number;
  personInfo: PersonInfo;
}

interface Student {
  studentId: number;
  personInfo: PersonInfo;
}

interface PersonInfo {
  name: string;
  age: number
}

How to create a SchoolReadonly type, in which all nested fields are readonly.

A simple test case:

var s: SchoolReadonly = {
  teachers: [{teacherId: 1, personInfo: {name: "John", age: 40}}],
  students: [{studentId: 1, personInfo: {name: "Dan", age: 20}}]
}

s.teachers[0].personInfo.name = "John2"; //should produce readonly error
s.students[0].personInfo.age = 22; //should produce readonly error

As a constraint, I do not want to add Readonly directly to the School, Teacher, Student and PersonInfo interfaces.


Solution

  • You can use generics and type aliases, it's a lot of code, but it will do the trick:

    interface School<T extends Teacher<PersonInfo>, S extends Student<PersonInfo>> {
        teachers: Array<T>;
        students: Array<S>;
    }
    
    type EditableSchool = School<EditableTeacher, EditableStudent>;
    type ReadonlySchool = Readonly<School<ReadonlyTeacher, ReadonlyStudent>>;
    
    interface Teacher<P extends PersonInfo> {
        teacherId: number;
        personInfo: P;
    }
    
    type EditableTeacher = Teacher<PersonInfo>;
    type ReadonlyTeacher = Readonly<Teacher<Readonly<PersonInfo>>>;
    
    interface Student<P extends PersonInfo> {
        studentId: number;
        personInfo: P;
    }
    
    type EditableStudent = Student<PersonInfo>;
    type ReadonlyStudent = Readonly<Student<Readonly<PersonInfo>>>;
    
    interface PersonInfo {
        name: string;
        age: number
    }
    
    var s: ReadonlySchool = {
      teachers: [{teacherId: 1, personInfo: {name: "John", age: 40}}],
      students: [{studentId: 1, personInfo: {name: "Dan", age: 20}}]
    }
    
    s.teachers = null;
    s.teachers[0].personInfo.name = "John2"; // Cannot assign to 'name' because it is a constant or a read-only property
    s.students[0].personInfo.age = 22; // Cannot assign to 'age' because it is a constant or a read-only property
    

    (code in playground)


    Edit

    If you want s.teachers[0] = nul to fail then you need to also change the Array to ReadonlyArray, so:

    interface School<T extends Teacher<PersonInfo>, Ta extends ArrayLike<T>, S extends Student<PersonInfo>, Ts extends ArrayLike<S>> {
        teachers: Ta;
        students: Ts;
    }
    
    type EditableSchool = School<EditableTeacher, Array<EditableTeacher>, EditableStudent, Array<EditableStudent>>;
    type ReadonlySchool = Readonly<School<ReadonlyTeacher, ReadonlyArray<ReadonlyTeacher>, ReadonlyStudent, ReadonlyArray<ReadonlyStudent>>>;
    

    The rest is the same, but then:

    s.teachers = null; // Cannot assign to 'teachers' because it is a constant or a read-only property
    s.teachers[0] = null; // Index signature in type 'ReadonlyArray<Readonly<Teacher<Readonly<PersonInfo>>>>' only permits reading
    

    True, it's not a simple solution and is very verbose, but I don't think that a simple generic solution exists, at least for now, for the problem.
    Consider:

    type ReadyonlyDeep<T> = {
        readonly [P in keyof T]: ReadyonlyDeep<T[P]>;
    }
    

    This works great as long as the object has only other simple objects:

    interface A {
        b: B
    }
    
    interface B {
        str: string;
    }
    
    let a: ReadyonlyDeep<A>;
    a.b.str = "fe"; // Cannot assign to 'str' because it is a constant or a read-only property
    

    But if you introduce an array:

    interface B {
        str: string;
        moreB: B[];
    }
    

    Then:

    a.b.moreB = []; // Cannot assign to 'moreB' because it is a constant or a read-only property
    a.b.moreB[0].str = "hey"; // this is fine though
    

    It probably isn't an easy problem to solve.