# Naming types in TypeScript: don’t!

If you have a React component named Foo, how do you name the type that represents its props? Do you use FooProps or just Props? I recently learned that I could’ve just named it Foo. No need to create a new name. So this is the convention I now use in React projects:

// ❌ Do not use "Props" — they are ambiguous, hard to auto-import, and
//    doesn’t work well when you have multiple components in the same file.
export interface Props { foo: string }
export const Thing: FC<Props> = (props) => { /* ... */ }

// ❌ Do not use "ThingProps" — verbose and requires an extra import.
export interface ThingProps { foo: string }
export const Thing: FC<ThingProps> = (props) => { /* ... */ }

// ✅ Use the same name for the component and the type representing its props.
export interface Thing { foo: string }
export const Thing: FC<Thing> = (props) => { /* ... */ }

At first glance, it may seem weird to create a type and a value with the same name. Wouldn’t that be confusing? This post provides the rationale for why this can be considered idiomatic code, as well as more examples.

# The two planes of existence

Did you know that this is a valid TypeScript code?

// Note: Contrived example; more useful examples below.
export type X = number
export const X = () => 42

We created a type and a constant, but they have the same name, yet we don’t get an error. This works because in TypeScript there is a world of values and another world of types.

When you declare things in TypeScript, you either:

  • Declare a name in the world of values. Examples: var, let, const, function.
  • Declare a name in the world of types. Examples: type, interface.
  • Declare a name in the both worlds. Examples: class[1], namespace[2], enum[3].
More explanation

If a name exists only in the world of values, it can’t be used as a type:

type X = 1
let Y = X
//      ^ 'X' only refers to a type, but is being used as a value here.

If a name exists only in the world of types, it can’t be used as a value:

let X = 1
type Y = X
//       ^ 'X' refers to a value, but is being used as a type here.
//         Did you mean 'typeof X'?

When importing a name, normally, both worlds are considered.

import { glob, IOptions } from 'glob'
//       |     |
//       |     +---> type
//       +---------> value

import { EventEmitter } from 'events'
//       |
//       +---> both type and value, since EventEmitter is a class

If import type is used, then only the world of types is considered.

import type { EventEmitter } from 'events'

function f(emitter: EventEmitter) {}
// Ok, as `EventEmitter` is used in a type context.

let x = new EventEmitter()
// 'EventEmitter' cannot be used as a value because it was imported
// using 'import type'.

# Why would this be useful? More real-world examples

As previously mentioned, whenever you create a class, a namespace, or an enum, you’re declaring a name in both the world of values and the world of types, and in different world they mean different things. Take the EventEmitter class, for example:

  • As a value, EventEmitter refers to the class object, the constructor function that creates instances of the class.
  • As a type, EventEmitter refers to the type that represents instances of the class.

We can also do the same thing in our own code. Let’s look at a few practical use cases. All of these examples came from real-world projects:

# Declaring a type with a corresponding schema

Sometimes we not only want to declare a type (which is only useful for compile-time checks), but also a schema (which is useful for runtime checks). Rather than having to use 2 different names, we can use the same name for both.

import { JSONSchemaType } from 'ajv'

// As a type, `RectangleSize` represents a shape of an object.
export interface RectangleSize {
  width: number
  height: number
}

// As a value, `RectangleSize` refers to a JSON schema corresponding to
// the type of the same name.
export const RectangleSize: JSONSchemaType<RectangleSize> = {
  type: 'object',
  properties: {
    width: { type: 'number' },
    height: { type: 'number' },
  },
  required: ['width', 'height'],
}

# Declaring an interface with a corresponding dependency injection token

This example is taken straight from VS Code’s source code (1)(opens new window) (2)(opens new window) (3)(opens new window).

// As a type, `ILoggerService` represents an interface that can be implemented
// by a service that can create loggers.
export interface ILoggerService {
  createLogger(file: URI, options?: ILoggerOptions): ILogger
  getLogger(file: URI): ILogger | undefined
}

// As a value, `ILoggerService` is a decorator that can be used to inject
// an `ILoggerService` instance into a class.
export const ILoggerService = createDecorator<ILoggerService>('loggerService')

// In another file...
import { ILoggerService } from 'vs/platform/log/common/log'
//       ^ imports both type and value

export class DependentService {
  constructor(@ILoggerService private readonly loggerService: ILoggerService) {}
  //           ^ value                                        ^ type
}

# Declaring a React component with a corresponding prop type

Are you tired of having to import 2 different names, one for the React component and another for the type representing its props?

import { Button, ButtonProps } from '../ui-kit'
//       |       |
//       |       +---> The prop type for the Button component
//       +-----------> The Button React component (value)

You can do this instead:

// The interface represents the props of the component
// (same name as the component!)
export interface Button {}

// The component itself
export const Button: React.FC<Button> = (props) => {
  /* ... */
}

And with that you no longer need to import the component and its prop type separately:

import { Button } from '../ui-kit'
//       |
//       +--> As a value, `Button` refers to the React component.
//            As a type, `Button` refers to the type representing Button’s props.
//            Both are imported at once!

Note that this pattern only works with functional components.

So, having learned this, in my recent React projects I now use the following convention:

// ❌ Do not use "Props" — they are ambiguous, hard to auto-import, and
//    doesn’t work well when you have multiple components in the same file.
export interface Props { foo: string }
export const Thing: FC<Props> = (props) => { /* ... */ }

// ❌ Do not use "ThingProps" — verbose and requires an extra import.
export interface ThingProps { foo: string }
export const Thing: FC<ThingProps> = (props) => { /* ... */ }

// ✅ Use the same name for the component and the type representing its props.
export interface Thing { foo: string }
export const Thing: FC<Thing> = (props) => { /* ... */ }

# Declaring a React context with a corresponding context type

The same can be applied to React contexts.

import { createContext } from 'react'

// The interface represents the type of the context value
// (same name as the context!)
export interface ThemeContext {
  theme: 'dark' | 'light'
}

// The context itself
export const ThemeContext = createContext<ThemeContext>({
  theme: 'dark',
})

# Automatic enforcement

To enforce the convention for React components, I use this Semgrep rule[4]:

rules:
  - id: consistent_react_prop_type_naming
    pattern-either:
      - pattern-regex: 'const (\w+): React\.FC<\s*(?:\1)?Props\s*>'
      - pattern-regex: 'function (\w+)\(\s*props:\s*(?:\1)?Props\s*'
    message: Make the prop type/interface have the same name as the React component.
    languages: [ts]
    severity: ERROR

  1. Take class X for example.

    • As a value, X refers to the constructor function.
    • As a type, X refers to the type that represents instances of the class.
    • To represent the type of the class itself, use typeof X.
    ↩︎
  2. Take namespace X for example.

    • As a value, X refers to the namespace object.
    • As a type, X refers to the type that contains the types exported from the namespace.
    • To represent the type of a namespace object itself, use typeof X.
    ↩︎
  3. Take enum X for example.

    • As a value, X refers to the enum object.
    • As a type, X refers to the type that represents the members of the enum.
    • To represent the type of the enum object itself, use typeof X.
    ↩︎
  4. In my projects, I use Semgrep in conjunction with ESLint. ESLint for community rules and Semgrep for project-specific rules. ↩︎