Skip to content

TypeScript Properties

This lesson focuses specifically on getting accurate, useful TypeScript types for reactive properties, covering common patterns like union types, optional fields, and object-shaped properties.

Typing @property() Fields

With the decorator syntax, TypeScript infers a property's type directly from its declared type annotation and default value, exactly like any other class field — @property() doesn't need you to repeat the type inside the decorator's options object (though type: Number/type: Boolean etc. inside the options object still control the runtime attribute conversion, which is a separate concern from the TypeScript type itself).

For properties with a restricted set of valid values — a variant prop that's only ever 'primary' | 'secondary' | 'danger' — union string literal types give you compile-time checking that a plain String type annotation alone wouldn't provide.

type Variant = 'primary' | 'secondary' | 'danger';

class ActionButton extends LitElement {
  @property() variant: Variant = 'primary'; // TS type: Variant; runtime attribute: String (default)
  @property({ type: Boolean }) disabled = false;
  @property({ attribute: false }) onSelect?: (id: string) => void;

  render() {
    return html`<button class=${this.variant} ?disabled=${this.disabled}>...</button>`;
  }
}

variant's TypeScript type (Variant) and its runtime attribute conversion (the default String behavior) are two independent, complementary layers of correctness.

Typing Different Property Shapes

@property() label: string = '';                  // simple string
@property({ type: Number }) count = 0;            // simple number
@property({ type: Boolean }) disabled = false;    // simple boolean
@property({ attribute: false }) items: Item[] = []; // array, no attribute
@property({ attribute: false }) user?: User;       // optional object, no attribute
  • For array/object properties, attribute: false is almost always correct — pair it with a real TypeScript type or interface.
  • Optional properties (user?: User) should generally default to undefined and be checked with this.user?.name in templates.
  • Union string literal types ('sm' | 'md' | 'lg') are a lightweight, effective way to type a small fixed set of valid string values.
  • The property's TypeScript type is independent from its type: option — type: controls runtime attribute conversion, not compile-time type checking.

Property Typing Patterns

Common shapes and how to type them well.

Shape Pattern
Fixed set of string values Union string literal type, e.g. 'sm' | 'md' | 'lg'
Optional object/array items?: Item[] or default to []/undefined
Required object/array (with attribute: false) @property({ attribute: false }) items: Item[] = [];
Callback/function property onSelect?: (id: string) => void; with attribute: false
Plain internal boolean flag @state() private isOpen = false;

Typing Fields When Using Plain static properties

If your project uses the plain static properties object instead of decorators, TypeScript doesn't automatically know those fields are reactive properties with real types. Use the declare keyword to add a type annotation for each field without generating a conflicting class field initializer.

class UserCard extends LitElement {
  static properties = {
    name: { type: String },
    age: { type: Number },
  };

  declare name: string;
  declare age: number;

  constructor() {
    super();
    this.name = '';
    this.age = 0;
  }
}

declare tells TypeScript the field's type exists and will be assigned elsewhere (here, in the constructor), without emitting its own field initializer that could conflict with static properties.

Exporting Property Types for Consumers

If other TypeScript code in your project (or a published package) constructs or type-checks against your component's properties, export any shared types (like the Variant union above) so consumers get the same compile-time checking your own component's internals benefit from.

// action-button.ts
export type Variant = 'primary' | 'secondary' | 'danger';

// consumer.ts
import type { Variant } from './action-button.js';
const myVariant: Variant = 'primary';

Common Mistakes

  • Using a plain string type for a property that really only accepts a small fixed set of values, missing out on compile-time checking a union type would provide.
  • Forgetting declare when adding TypeScript types to fields defined through the plain static properties object, causing field initialization conflicts.
  • Not exporting shared prop types, forcing consumers to redefine the same union/interface themselves.
  • Confusing a property's type: runtime option (attribute conversion) with its TypeScript compile-time type — they serve different purposes and are configured independently.

Key Takeaways

  • Decorator-based properties get their TypeScript type from the field's own type annotation and default value.
  • Union string literal types are an easy, effective way to type properties with a small fixed set of valid values.
  • declare is needed to add TypeScript types to fields defined through the plain static properties object.
  • Export shared property types so other TypeScript code consuming your component benefits from the same type safety.

Pro Tip

As soon as a string property's valid values are countable (a size, variant, or status prop), convert it to a union string literal type immediately — it's one of the highest-value, lowest-effort type safety improvements available in a Lit + TypeScript component.