Home > OS >  Can't extends Generic for TypeScript class
Can't extends Generic for TypeScript class

Time:01-12

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.


Playground

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]' });
  • Related