1

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!

2 Answers 2

2

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

Sign up to request clarification or add additional context in comments.

5 Comments

@Tobais S., thank you for your answer. Is there a way to workaround this? How can I set a default value for data if arg is missing?
This really depends on how you would want to handle the unsoundness. We could wrap data into a Partial indicating that any properties might not exist when trying to access them: tsplay.dev/wRzg1w.
@Tobais S, in the example that I provided, is it not allowed because the type parameter 'T' is narrower than type '{}'?
I don't think there is a subtype of type {[key: string]: any} that doesn't satisfy type {} since an instance of type {} can take in anything except null & undefined. Please correct me if I am wrong.
@DinhMinhLuu - it is not allowed because {} and T might be different types.
1

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.