2

I'm fairly new to strongly typed languages, coming from the "jungle" world of JS and PHP. In my current Typescript project(React-Typescript), i'm struggling with a very fundamental question: Should every piece of code expose its own interface, even if that means having duplicate types across my application?

Simple example:

//API
interface TodoInterface {
  id: number,
  title: string,
  completed: boolean
}
function getTodos(): Array<TodoInterface> {
  return [{ id: 1, title: 'title', completed: true }, { id: 2, title: 'title', completed: true }]]
}

//Store
interface TodoStoreTodo {
  id: number,
  title: string,
  completed: boolean
}

class TodoStore {
  todos!: Array<TodoStoreTodo>

  async getTodos() {
    const todos = await getTodos()
    this.todos = todos;//Types are compatible, so this works.
  }
}

//main view
const todosStore = new TodoStore()
const Todos = () => {
  const { todos } = todosStore
  return (
    todos.map(todo => <Todo todo={todo} />)
  )
}

//Todo view
interface TodoProps {
  todo: {
    id: number
    title: string
    completed: boolean
  }
}


const Todo: React.FC<TodoProps> = (props) => {
  const { todo } = props;
  return (
    <div>
      <p>Id: {todo.id}</p>
      <p>Title: {todo.title}</p>
      <p>Completed: {todo.completed}</p>
    </div>
  )
}

Note that the same exact Todo "entity" is declared in 3 different places: The API function that fetches todos, the store that stores todos and the Todo component.

On one hand, this makes sure that each module/function/component is independent and "blind" to the outside world, but on the other hand this creates some repetition(not of logic of course, but still)

A radical alternative to this, would of course be having some global Todo interface, which will be "used" by all relevant parts.

It's like, there is some struggle between DRY and the will to avoid spaghetti code.

Any suggestions, explanations, or tips will be highly appreciated.

1 Answer 1

6

The same design concepts apply to types and runtime variables.

Depending on the relation and diversity of these WET types, base extendable and/or generic common types can be defined. It's not incorrect to define them for the whole application or its part, but as opposed to global types, common types are supposed to exported from a shared module.

E.g:

// base type to extend
export interface BaseTodo<T extends any> {
  id: number,
  title: string,
  completed: boolean,
  meta?: T,
}

// most commonly used type with no meta
export type SimpleTodo = BaseTodo<never>;

And imported where they are used:

import type { BaseTodo } from '.../common/types/todo.ts';

type PublishedTodo = BaseTodo<{ published: boolean }>;

// Since it's not practical to exclude id from common type,
// this can be done for a specific type
interface PublishedTodoDraft extends Omit<PublishedTodo, 'id'> {
  edited: Date
};
3
  • So you would place a generic Toto type, somewhere in the root of the application? But what happens if suddenly some "consumer" of this type needs to slightly change its structure? For example, imagine you have some property that references other types. In one place the reference is an array of ids(foreign keys), whereas in another place it's an array of the referenced objects(the entire referenced entity?
    – i.brod
    Oct 5, 2021 at 22:14
  • You can extend a base one with no changes if generic params cover this (the example with meta), or partially extend it they don't (e.g. use Omit util for incompatible keys), or create a new type if the difference with base one isn't worth extending. It's really the same as with runtime JS things like classes. Oct 5, 2021 at 22:19
  • I added a clarification. I guess this can be translated easily to your case with references. Oct 6, 2021 at 8:05

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.