I am having trouble understanding this behavior of TypeScript
Generic with classes.
TYPESCRIPT
interface IProvider<K extends {[key: string]: any}> {
data: K;
}
class Provider<T extends {[key: string]: any}> implements IProvider<T> {
data: T;
constructor(arg?: T) {
this.data = arg || {}; // This is not allowed.
}
}
type User = {
[key: string]: any
}
const x = new Provider<User>();
The error goes:
Type 'T | {}' is not assignable to type 'T'.
'T | {}' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{ [key: string]: any; }'.
Type '{}' is not assignable to type 'T'.
'{}' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{ [key: string]: any; }'.
However, if I remove the optional operator, it works fine.
TYPESCRIPT
class Provider<T extends {[key: string]: any}> implements IProvider<T> {
data: T;
constructor(arg: T) { // no optional
this.data = arg || {}; // Now it works.
}
}
Please help me explain this. Thank you very much!
CodePudding user response:
The error is rightfully warning you of potential unsoundness.
Consider the following scenario where the User
type has a property a
of type string
. When arg
is optional, we don't have to pass any object to the constructor which would initialize data with {}
.
Accessing x.data.a
would lead to a runtime value of undefined
, even though we typed it as string
.
type User = {
a: string
}
const x = new Provider<User>();
x.data.a.charCodeAt(0) // runtime Error!
This can not happen if we make the constructor parameter mandatory.
CodePudding user response:
Today is your lucky day.
I do not want to talk about why it is not working but explain the concept of generics, why, and where should we use it.
1- Behavior
For example, I have 3 objects,
- Product
- User
- Contact
I have a Printer class that can print any object that implements a Printable interface like this.
export interface Printable {
print(): string;
}
export interface Printer<T extends Printable> {
print(obj: T): string;
}
export class BlackWhitePrinter<T extends Printable> {
print(obj: T) {
return `[BlackWhitePrinter] ` obj.print();
}
}
export class ColorPrinter<T extends Printable> {
print(obj: T) {
return `[Color Printer] ` obj.print();
}
}
export class Product implements Printable {
readonly name: string = 'product name';
print() {
return this.name;
}
}
export class User implements Printable {
readonly username: string = 'username';
print() {
return this.username;
}
}
export class Contact implements Printable {
readonly phone: string = ' 1 999 999 99 99';
print() {
return this.phone;
}
}
const blackWhitePrinter = new BlackWhitePrinter();
const colorPrinter = new BlackWhitePrinter();
blackWhitePrinter.print(new User());
blackWhitePrinter.print(new Product());
blackWhitePrinter.print(new Contact());
colorPrinter.print(new User());
colorPrinter.print(new Product());
colorPrinter.print(new Contact());
2- Data and Behavior
interface PhoneNumber {
phoneNumber?: string;
}
interface EmailAddress {
email?: string;
}
interface CanCall {
call(contact: PhoneNumber): void;
}
interface CanEmail {
email(contact: EmailAddress): void;
}
interface Contact extends PhoneNumber, EmailAddress {}
interface ContactWithAddress extends Contact {
address?: string;
}
/**
* Android phone can call and send email
*/
export class AndroidPhone<T extends ContactWithAddress>
implements CanCall, CanEmail
{
constructor(public readonly contacts: T[]) {}
call(contact: PhoneNumber): void {
console.log(`Call to ${contact.phoneNumber}`);
}
email(contact: EmailAddress): void {
console.log(`Email to ${contact.email}`);
}
}
/**
* Regular phone can call only
*/
export class RegularPhone<T extends PhoneNumber> implements CanCall {
constructor(public readonly contacts: T[]) {}
call(contact: PhoneNumber): void {
console.log(`Calling to ${contact.phoneNumber}`);
}
}
/**
* Unfortunately, some people only have regular phones.
*/
class PoorUser {
constructor(public readonly phone: CanCall) {}
}
/**
* Some dudes, always use the last vertion of XYZ Smart phones
*/
class RichUser<T extends CanCall & CanEmail> {
constructor(public readonly phone: T) {}
}
const poorUser = new PoorUser(
new RegularPhone([{ phoneNumber: ' 1 999 999 99 99' }])
);
/**
* Even after we give a smart phone to poor people, they cannot send emails because they do not have internet connection :(
*/
const poorUser1 = new PoorUser(
new AndroidPhone([{ phoneNumber: ' 1 999 999 99 99' }])
);
/**
* Hopefully, they can call if they paid the bill.
*/
poorUser.phone.call({ phoneNumber: ' 1 999 999 99 99' });
// poorUser1.phone.email({email:'.......'}) // Cannot send email because he is not aware of the future!
/**
* Rich people neither call nor email others because they are always busy and they never die.
*/
const richUser = new RichUser(
new AndroidPhone([
{ email: '[email protected]', phoneNumber: ' 1 999 999 99 99' },
])
);
/**
* Another rich call.
*/
richUser.phone.call({ phoneNumber: ' 1 999 999 99 99' });
/**
* Another rich email. If you are getting lots of emails, it means you are
* poor because rich people do not open their emails, their employees do.
* I've never seen any rich googling or searching in StackOverflow "How to replace Money with Gold?", probably they search things like that. Did you see any?
*/
richUser.phone.email({ email: '[email protected]' });