Как я могу гарантировать, что свойство readonly НЕ присваивается изменяемому? - PullRequest
2 голосов
/ 12 июля 2020

Предположим, у меня есть два типа A и B в TypeScript, и я не хочу, чтобы значения типа A можно было присвоить B.

I хотел бы принудительно применить это, так что если один (или оба) из типов случайно изменятся, чтобы разрешить назначение, мы получим ошибку компиляции или сбой теста. Есть ли способ выполнить sh это в TypeScript?

Конечно, легко добиться того, чтобы присваивание было законным, просто выполнив присваивание в тесте. Но я не могу сразу увидеть способ обеспечить, чтобы конкретный фрагмент кода не проходил проверку типа TypeScript.

Вот еще кое-что о том, почему я хочу это сделать. Я хочу обеспечить неизменность определенного типа, примерно так:

interface MyImmutableThing {
    readonly myfield: string;
}

Однако эта проблема делает эту проблему c, потому что если у меня есть идентичный в остальном изменяемый тип например:

interface MyThing {
    myfield: string;
}

, тогда значения типа MyImmutableThing могут быть присвоены MyThing, что позволяет обойти безопасность типов и изменить myfield. Следующий код компилируется, запускается и вызывает изменение imm.myfield:

const imm: MyImmutableThing = {myfield: 'mumble'};
const mut: MyThing = imm;
mut.myfield = 'something else';

Я не знаю способа надежно обеспечить неизменность такого типа во время компиляции, но я могу по крайней мере реализовать принудительное исполнение во время выполнения с использованием вместо этого класса, например:

class MyImmutableThing {
    private _myfield: string;
    get myfield(): string { return this._myfield; }
    constructor(f: string) { this._myfield = f; }
}

Тогда, хотя код, подобный приведенному ниже, все равно будет компилироваться, это приведет к ошибке времени выполнения: затем напишите тест, который утверждает, что возникает эта ошибка времени выполнения.

Однако, если мое поле имеет тип массива (или кортежа), ситуация меняется:

interface MyArrayThing {
    myfield: string[];
}
interface MyImmutableArrayThing {
    readonly myfield: readonly string[];
}

Теперь значение MyImmutableArrayThing не присваивается MyArrayThing из-за readonly типа массива. Следующее не будет компилироваться:

const imm: MyImmutableArrayThing = {myfield: ['thing']};
const mut: MyArrayThing = imm;

Это хорошо, поскольку дает нам больше уверенности в неизменности во время компиляции, чем мы получили с полем string. Однако теперь сложнее написать тесты, которые фиксируют наше намерение здесь, или иным образом обеспечить его выполнение.

Невозможность присваивания MyImmutableArrayThing s на MyArrayThing является ключом к системе типов, обеспечивающей требуемые свойства. , но как нам помешать кому-либо внести какие-либо изменения, например добавить readonly к массиву в MyArrayThing, разрешить что-то подобное и нарушить требуемое свойство? на данный момент довольно сбивает с толку, поэтому возможность делать утверждения такого рода была бы весьма полезна для предотвращения регрессов.

Вот ссылка на TypeScript Playground для кода в этом вопросе.

Ответы [ 2 ]

3 голосов
/ 13 июля 2020

Модификатор readonly в настоящее время не участвует в проверках типа присваивания. Не существует конструкции на уровне языка, которая допускала бы ошибки при присвоении свойств readonly изменяемым.

Другими словами, поэтому ваш пример компилируется без ошибок:
const imm: MyImmutableThing = { myfield: 'mumble' };
const mut: MyThing = imm; // readonly `myfield` is assignable to mutable one
mut.myfield = 'something else'; // duh! Changed a readonly declared property :/

Почему readonly string[] ошибается правильно?

readonly string[] - это сокращенная форма для ReadonlyArray. ReadonlyArray имеет собственные определения типов и фактически является супертипом из Array (с меньшим количеством свойств). Таким образом, может сработать обычная проверка совместимости типов, которая запрещает присваивать более широкий тип более узкому:

// We can check above statement directly with TS types
type IsROArrayAssignableToArray = ReadonlyArray<any> extends Array<any> ? 1 : 0 // 0
type IsArrayAssignableToROArray = Array<any> extends ReadonlyArray<any> ? 1 : 0 // 1

Обходной путь

Пока компилятор не сможет проверять эти вещи (давайте дадим за проблему голосование), мы можем использовать правило линтинга, например total-functions/no-unsafe-assignment from eslint-plugin-total-functions.

Minimal .eslintr c. json:
{
  "extends": [
    "plugin:total-functions/recommended"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": "./tsconfig.json"
  },
  "plugins": ["@typescript-eslint", "total-functions"]
}
package. json:
"devDependencies": {
  "@typescript-eslint/eslint-plugin": "^3.6.0",
  "@typescript-eslint/parser": "^3.6.0",
  "eslint": "^7.4.0",
  "eslint-plugin-total-functions": "^1.35.2",
  "typescript": "^3.9.6"
}
// ...
Теперь все ваши случаи выше испускают и ошибку ESLint, по желанию :
const imm: MyImmutableThing = { myfield: "mumble" }
const mut: MyThing = imm // error:
// Using a readonly type to initialize a mutable type can lead to unexpected mutation 
// in the readonly value. eslint(total-functions/no-unsafe-assignment)
3 голосов
/ 13 июля 2020
• 1000 с игровой площадки TypeScript для получения более подробной информации.

В вашем случае фирменное оформление шрифта может выглядеть примерно так:

interface ImmutableThing {
  readonly myfield: string
  __brand: "ImmutableThing"
}

interface MutableThing {
  myfield: string
  __brand: "MutableThing"
}

const imm: ImmutableThing = {myfield: "thing"} as ImmutableThing;
const mut: MutableThing = imm; // type error
mut.myfield = "mutated"; 

Ссылка на игровую площадку

Если если вас интересует брендирование шрифтов, ознакомьтесь с ts-brand для более продвинутого использования.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...