When string literal type defined in interface I get unexpected behaviors.
interface IFoo {
value: 'foo' | 'boo';
}
When I implement interface in class I get error:
class Foo implements IFoo {
value = 'foo';
}
I get an error: Property 'value' in type 'Foo' is not assignable to the same property in base type 'IFoo'. But 'foo' is correct value for string literal.
On other hand:
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';
This code doesn't cause any errors, but the Boo.value
is of any
type. I'v expected to get an error that Boo doesn't implement IFoo, but there is no any error.
The only correct way I found out is to implement classes that way:
class Koo implements IFoo {
value: 'foo' | 'boo' = 'foo';
}
So I had to declare 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;
I understand this happend because ts compiller got Doo.value type from assigned value in field declaration. Looks like it useless to declare fields of string literal types in interfaces, or I'm doing something wrong. And also figured out that classes can implement interfaces with type any for fields, so it's up to developer.
The problem is you expect implements IFoo
to impact the way the class field is typed. It does not. The way things happen, is the class fields are typed as if the implements Foo
does not exist and after the class type has been fully resolved, it is checked for compatibility with the implemented interfaces. Looked at this in this way, the errors make sense.
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
}
}
When you use an enum, things work because if we assign a value of the enum to the field, the field will be typed as the enum.
You can specify the type of the value
field, either explicitly or relatinve to the IFoo
interface:
class Foo implements IFoo {
value: IFoo['value'] = 'foo';
}
Or if the field is readonly
it will be typed as the string literal type:
class Foo implements IFoo {
readonly value = 'foo';
}