DEV Community

Discussion on: Types vs. Interfaces in Typescript

Collapse
 
larsejaas profile image
Lars Ejaas

Wow appreciate your feedback. But, I think must of the stuff you described here is a bit beond my current skill level. I use typescript with React for now, and I have to admit I feel unsure why your above example wouldn't have worked equally well with "interface" instead of "types" 🤔

Thread Thread
 
peerreynders profile image
peerreynders

why your above example wouldn't have worked equally well with "interface" instead of "types" 🤔

Actually it's easier to explore when to use interface instead of type:

  • interface declarations merge, type aliases do not. So if it is necessary to declare an interface in bits-and-pieces interface is the only choice especially when monkey patching an existing class (which really should be avoided for built-in and even third party classes). With type each piece needs to be a separate type which are then combined by intersecting them.

  • By convention use interface not type when the declaration is going to be implemented by a class:

While syntactically correct

type Title = {
  title: string;
};

class Dog implements Title {
  #title: string;

  constructor(breed: string, name: string) {
    this.#title = `${name} (${breed})`;
  }

  get title(): string {
    return this.#title;
  }
}

const fido = new Dog('Maltese', 'Froufrou');
const expected = 'Froufrou (Maltese)';
console.assert(fido.title === expected, `Title not "${expected}"`);
Enter fullscreen mode Exit fullscreen mode

a class should implement an interface, not a type:

interface Title {
  title: string;
};

class Dog implements Title {
  #title: string;

  constructor(breed: string, name: string) {
    this.#title = `${name} (${breed})`;
  }

  get title(): string {
    return this.#title;
  }
}

const fido = new Dog('Maltese', 'Froufrou');
const expected = 'Froufrou (Maltese)';
console.assert(fido.title === expected, `Title not "${expected}"`);
Enter fullscreen mode Exit fullscreen mode

Everywhere else use type to get full access to TypeScript's typing features.

So for object types that are not class-based it makes sense to use type.

type Key = 'A' | 'B' | 'C' | 'D' | 'E'; // Can't do this with `interface` because `Key` isn't an object type
type Value = 1 | 2 | 3 | 4 | 5;
type Entry = [Key, Value];              // Again a tuple isn't an object type - so `interface` is no help here
                                        // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-3.html#tuple-types
Enter fullscreen mode Exit fullscreen mode

Derived object types become type aliases, not interfaces:

const fido = { 
  breed: 'Maltese', 
  name: 'Froufrou',
  title: () => `${fido.name} (${fido.breed})`
};

type Dog = typeof fido; // type Dog = { breed: string, name: string, title: () => string }

const expected = 'Froufrou (Maltese)';
console.assert(fido.title() === expected, `Title not "${expected}"`);
Enter fullscreen mode Exit fullscreen mode

type supports Mapped Types and Generics:

type KeyName = 'name' | 'breed'
type Dog<T> = {
  [key in KeyName]: T;
}

{
  const name = 'Froufrou';
  const breed = 'Maltese';
                     // type inference:
  const dog = {      // const dog: { name: string, breed: string }
    name,
    breed 
  };
                             // type inference:
  const fido:Dog<string> = { // const fido: Dog<string>
    name,
    breed 
  };

  console.assert(name === dog.name && breed === dog.breed, 'dog: No match')
  console.assert(name === fido.name && breed === fido.breed, 'fido: No match')
}
Enter fullscreen mode Exit fullscreen mode

Most of the Utility Types are defined via type aliases.

Because of the versatility of type aliases open source projects like comlink use them heavily - so being able to decipher type aliases can be helpful to understand the type constraints.

By limiting yourself to interface you aren't leveraging TypeScript's features as much as you could.

People usually get into type aliases once they realize how useful sum types (Union types) really are.

const left: unique symbol = Symbol('Left');
const right: unique symbol = Symbol('Right');

// Discriminating Union + Generics
type Either<L,R> = [typeof left, L] | [typeof right, R];

function showResult(result : Either<string,number>): void {
  switch(result[0]) {
    case left:
      console.log('Error (Left):', result[1]);
      break;
    case right:
      console.log('Success (Right):', result[1].toFixed(2));
      break;
  }
}

function validate(value: number): Either<string, number> {
  return value < 10 ? [right, value] : [left, 'Too Large'];
}

showResult(validate(Math.PI)); // 'Success (Right): 3.14'
showResult(validate(10));      // 'Error (Left): Too Large'
Enter fullscreen mode Exit fullscreen mode

Another nifty thing one can do with type:

// Only exists in the "type context"
declare const emailVerified: unique symbol;

// Make structurally different from plain `string`
type Email = string & {
  [emailVerified]: true
}

const verifiedEmails = new Set(['jane.doe@example.com']);

// Assertion function
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions
function assertIsEmail(email: string): asserts email is Email {
  if (!verifiedEmails.has(email)) throw new Error(`"${email}" is not a verified email`);
} 

// export this function
function validateEmail(email: string): Email {
  assertIsEmail(email);   // `email: string`
  return email;           // `email: Email`
}

// Simple type alias
type EmailAlias = string;

try {
  const email = 'jane.doe@example.com';
  const verified = validateEmail(email); // const verified: Email
  console.log('Verified:', verified);    // 'Verified: jane.doe@example.com'

  const another = 'john.doe@example.net';
  const unverified: EmailAlias = another; // Simple type alias **will not** cause an error; however
  // const unverified: Email = another;   // Type 'string' is not assignable to type 'Email'.
                                          // Type 'string' is not assignable to type '{ [emailVerified]: true; }'.(2322)
  const forced: Email = another as Email; // However type assertion can silence the error on `Email` and narrow the type.
                                          // https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions
  console.log('Forced:', forced);         // 'Forced: john.doe@example.net'

  const notVerified = validateEmail(another);
  // Never gets here
  console.log('Unverified', unverified);

} catch(e) {
  console.log(e.message);                // "john.doe@example.net" is not a verified email
}
Enter fullscreen mode Exit fullscreen mode

In TypeScript's type context Email is structurally different from string - even though in JavaScript's value context it simply is a string.

This can help prevent a regular string from being assigned to Email without being validated (though a type assertion can force it) - without having to resort to a holder object:

type Email = {
  email: string;
}

const verifiedEmails = new Set(['jane.doe@example.com']);

function validateEmail(email: string): Email {
  if (!verifiedEmails.has(email)) throw new Error(`"${email}" is not a verified email`);
  return {
    email
  };
}

try {
  const email = 'jane.doe@example.com';
  const verified = validateEmail(email);    // const verified: Email
  console.log('Verified:', verified.email); // 'Verified: jane.doe@example.com'

  const another = 'john.doe@example.net';
  const forged: Email = { email: another };
  console.log('Forged:', forged.email );    // 'Forged: john.doe@example.net'

  const notVerified = validateEmail(another);
  // never gets here

} catch(e) {
  console.log(e.message);                // "john.doe@example.net" is not a verified email
}
Enter fullscreen mode Exit fullscreen mode

So types go beyond classes and interfaces. If you're not using type what are you using TypeScript for?