Ошибка назначения поля с типизированным строковым литералом при определении в интерфейсе

Когда в интерфейсе определен строковый литерал, я получаю неожиданное поведение.

interface IFoo {
    value: 'foo' | 'boo';
}

Когда я реализую интерфейс в классе, я получаю ошибку:

class Foo implements IFoo {
    value = 'foo';
}

Я получаю сообщение об ошибке: «Значение» свойства в типе «Foo» не может быть присвоено тому же свойству в базовом типе «IFoo». Но "foo" - правильное значение для строкового литерала.

С другой стороны:

class Boo implements IFoo {
    value;
    constructor() {
        this.value = 'foo';
        this.value = 'boo';
        this.value = 'koo'; // must be an error Boo doesn't implement IFoo
    }
}
const test = new Boo();
test.value = 'koo';

Этот код не вызывает ошибок, но Boo.value имеет тип any. Я ожидал получить ошибку, что Boo не реализует IFoo, но ошибки нет.

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

class Koo implements IFoo {
    value: 'foo' | 'boo' = 'foo';
}

Поэтому мне пришлось объявить enum:

enum Doos { foo = 'foo', boo = 'boo' }
interface IDoo {
    value: Doos;
}
class Doo implements IDoo {
    value = Doos.foo;
}
const test = new Doo();
test.value = Doos.boo;

Я понимаю это, потому что компилятор ts получил тип Doo.value из присвоенного значения в объявлении поля. Похоже, что объявлять поля строковых литералов в интерфейсах бесполезно, или я что-то делаю не так. А также выяснил, что классы могут реализовывать интерфейсы с любым типом для полей, так что это зависит от разработчика.


person Dmitriy Snitko    schedule 19.09.2018    source источник


Ответы (1)


Проблема в том, что вы ожидаете, что implements IFoo повлияет на способ ввода поля класса. Это не. Дело в том, что поля класса набираются так, как будто implements Foo не существует, и после того, как тип класса был полностью определен, он проверяется на совместимость с реализованными интерфейсами. Если посмотреть на это с этой точки зрения, ошибки имеют смысл.

class Foo implements IFoo {
    value = 'foo'; // this is typed as string, not as the string literal type and thus is not compatible with value in IFoo 
}
class Boo implements IFoo {
    // no type, no init, value is typed as any and any is compatible with 'foo' | 'boo' 
    // use -noImplicitAny to avoid such errors
    value; 
    constructor() {
        this.value = 'foo';
        this.value = 'boo';
        this.value = 'koo'; // 'koo' is compatible with any
    }
}

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

Вы можете указать тип поля value либо явно, либо по отношению к интерфейсу IFoo:

class Foo implements IFoo {
    value: IFoo['value'] = 'foo';
}

Или, если поле readonly, оно будет набрано как строковый литерал:

class Foo implements IFoo {
    readonly value = 'foo';
}
person Titian Cernicova-Dragomir    schedule 19.09.2018