Type System Differences in TypeScript (Structural Type System) VS C# & Java (Nominal Type System)

Type System Differences in TypeScript (Structural Type System) VS C# & Java (Nominal Type System)

When a developer with C# or Java background learns TypeScript, there is a temptation to write TypeScript with the same style as C# or Java. It is easy to lose TypeScript's beauty by not understanding the type system TypeScript offers and how it differs from languages that are already familiar to the developer. One of the differences is that TypeScript uses Structural Subtyping. C# and Java both use Nominal type system. In this blog post, I'll cover basics of both systems and few examples, enjoy!

Nomen est omen

Nominal refers to Latin word nomen, the name. Many might be familiar with phrase nomen est omen (the name is a sign) or nomen est omnis (the name is everything) and the especially the latter phrase can be applied to programming context also. The name of the type defines is the type compatible or not, for example, as a method argument.

Let's look at a C# example. 3rd party UI library contains a drop-down component and it will take a list of options.

public class SpecialOption
{
    public string Name { get; set; }
    public int Id { get; set;}
}

The user of the library wants to display a list of companies and the backend already has a service that returns a list of company names together with ids.

public class Company
{
    public string Name { get; set; }
    public int Id { get; set; }
}

public class CompanyService
{
    public IEnumerable<Company> GetAll() {
        ...		
    }
}

Our class Company and SpecialOption are identical, but unfortunately, list of companies are not compatible as they are considered different types.

There are several options to fix this:

  • make Company and SpecialOption compatible by making them implement the same interface
  • make Company and SpecialOption compatible by making them inherit the same base class
  • loop all elements and map Company to SpecialOption

Unfortunately, only the latter is a valid option as 3rd party libraries API interfaces and types cannot be changed.

Structural typing

In TypeScript, the interface tells what is the structure that is accepted. The previous example could be written like this.

interface Company {
  name: string
  id: number
}

interface SpecialOption {
  name: string
  id: number
}

const companies:ReadonlyArray<Company> = [{
  id: 1,
  name: 'AcmeCorp'
}]

const DropdownList = ({ options }:{ options: ReadonlyArray<Company>}) => {
}

DropdownList({
  options: companies // <- no errors as the structure matches, name doesn't matter
})

The TypeScript example has a bit more lines because I made a mock UI control + initialization. The main thing is that companies are valid options. If I change a property name on the Company to the title, there will be an error, so I still have type-safety.

A bigger real-world example could be from the React app.

Let's imagine a scenario where I need to support different modes (read/edit) for each type of component. Each component should have read/edit modes defined.

Photos -> View
-> Edit

Maps -> View
-> Edit

Feedback -> Edit <- should give a compile-time error as the View is not implemented

...place dozen components here, etc.

One way to model the solution is to use structural typing.

import ViewComponents from './View'
import EditComponents from './Edit'

const renderers = {
  [RenderType.View]: ViewComponents,
  [RenderType.Edit]: EditComponents 
}

const render = (
  data: FormModule,
  renderType: RenderType
): JSX.Element => {
  const renderer = renderers[renderType]
  switch (data.type) {
    case ComponentType.Photos:
      return <renderer.Photos data={data} />
    case ComponentType.Maps:
      return <renderer.Maps data={data} />
    default:
      throw new Error('Not implemented')
  }
}

The idea is that when a developer implements new component then she/he needs to implement both View and Edit, otherwise there will be an error.

How is this done without any interfaces?

These two lines need to import objects that have the same structure:

import ViewComponents from './View'
import EditComponents from './Edit'

In my case, View and Edit export the right components.

View folder -> index.tsx

import Photos from './Photos'
import Maps from './Maps'

export default {
  photos: Photos,
  maps: Maps
}

---

Edit folder -> index.tsx

import Photos from './Photos'
import Maps from './Maps'

export default {
  photos: Photos,
  maps: Maps
}

The renderer structure needs to be same and then the component can be rendered:

const renderer = renderers[renderType]
return <renderer.Photos data={data} /> // <- there would be error if Photos would not be in either View or Edit renderer

--

this is how the renderers data looks:

renderers: {
  view: {
    Photos: PhotosViewComponent,
    Maps: MapsViewComponent
  },
  edit: {
    Photos: PhotosEditComponent,
    Maps: MapsEditComponent
  }
}

Conclusion

I hope the provided examples help understand the difference between nominal and structural typing. Maybe in your codebase, there are unnecessary complex mappings or inheritance that could be simplified using some benefits that structural typing brings.

Nominal typing has its own merits and as the Wikipedia page of nominal typing points out, there are some costs:

Nominal typing is useful at preventing accidental type equivalence, which allows better type-safety than structural typing. The cost is a reduced flexibility, as, for example, nominal typing does not allow new super-types to be created without modification of the existing subtypes.

It's our duty as developers to understand pros and cons of each approach. To make proper use of pros and understand cons leads to elegant solutions. Happy coding!

Feedback and discussion on Hacker News