Headshot of Marius Schulz
Marius Schulz Front End Engineer

TypeScript 2.4: Weak Type Detection

TypeScript 2.4 introduced the concept of weak types. A type is considered weak if all of its properties are optional. More specifically, a weak type defines one or more optional properties, no required properties, and no index signatures.

For example, the following type is considered a weak type:

interface PrettierConfig {
  printWidth?: number;
  tabWidth?: number;
  semi?: boolean;
}

The main goal of weak type detection is to find likely errors in your code that would otherwise be silent bugs. Consider this example:

interface PrettierConfig {
  printWidth?: number;
  tabWidth?: number;
  semi?: boolean;
}

function createFormatter(config: PrettierConfig) {
  // ...
}

const prettierConfig = {
  semicolons: true
};

const formatter = createFormatter(prettierConfig); // Error

Before TypeScript 2.4, this piece of code was type-correct. All properties of PrettierConfig are optional, so it's perfectly valid not to specify any of them. Instead, our prettierConfig object has a semicolons property which doesn't exist on the PrettierConfig type.

Starting with TypeScript 2.4, it's now an error to assign anything to a weak type when there's no overlap in properties (see the documentation). The type checker errors with the following message:

Type '{ semicolons: boolean; }' has no properties
in common with type 'PrettierConfig'.

While our code is not strictly wrong, it likely contains a silent bug. The createFormatter function will probably ignore any properties of config that it doesn't know (such as semicolons) and fall back to the default values for each property. In this case, our semicolons property doesn't have any effect, no matter if it's set to true or false.

TypeScript's weak type detection helps us out here and raises a type error for the prettierConfig argument within the function call. This way, we're made aware quickly that something doesn't look right.

Explicit Type Annotations

Instead of relying on weak type detection, we could explicitly add a type annotation to the prettierConfig object:

const prettierConfig: PrettierConfig = {
  semicolons: true // Error
};

const formatter = createFormatter(prettierConfig);

With this type annotation in place, we get the following type error:

Object literal may only specify known properties,
and 'semicolons' does not exist in type 'PrettierConfig'.

This way, the type error stays local. It shows up in the line in which we (incorrectly) define the semicolons property, not in the line in which we (correctly) pass the prettierConfig argument to the createFormatter function.

Another benefit is that the TypeScript language service can give us autocompletion suggestions because the type annotation tells it what type of object we're creating.

Workarounds for Weak Types

What if, for some reason, we don't want to get errors from weak type detection for a specific weak type? One workaround is to add an index signature to that type:

interface PrettierConfig {
  [prop: string]: any;
  printWidth?: number;
  tabWidth?: number;
  semi?: boolean;
}

function createFormatter(config: PrettierConfig) {
  // ...
}

const prettierConfig = {
  semicolons: true
};

const formatter = createFormatter(prettierConfig);

Now, this piece of code is type-correct because we explicitly allow properties of unknown names in our PrettierConfig type.

Alternatively, we could use a type assertion to tell the type checker to treat our prettierConfig object as if it were of type PrettierConfig:

interface PrettierConfig {
  printWidth?: number;
  tabWidth?: number;
  semi?: boolean;
}

function createFormatter(config: PrettierConfig) {
  // ...
}

const prettierConfig = {
  semicolons: true
};

const formatter = createFormatter(
  prettierConfig as PrettierConfig
);

I recommend you stay away from using type assertions to silence weak type detection. Maybe there's a use case where this escape hatch makes sense, but in general, you should prefer one of the other solutions.

The Limits of Weak Type Detection

Note that weak type detection only produces a type error if there's no overlap in properties at all. As soon as you specify one or more properties that are defined in the weak type, the compiler will no longer raise a type error:

interface PrettierConfig {
  printWidth?: number;
  tabWidth?: number;
  semi?: boolean;
}

function createFormatter(config: PrettierConfig) {
  // ...
}

const prettierConfig = {
  printWidth: 100,
  semicolons: true
};

const formatter = createFormatter(prettierConfig);

In the above example, I specified both printWidth and semicolons. Because printWidth exists in PrettierConfig, there's now a property overlap between my object and the PrettierConfig type, and weak type detection no longer raises a type error for the function call.

The takeaway here is that the heuristics behind weak type detection are designed to minimize the number of false positives (correct usages treated as incorrect), which comes at the expense of fewer true positives (incorrect usages treated as incorrect).

This post is part of the TypeScript Evolution series:

  1. TypeScript 2.0: Non-Nullable Types
  2. TypeScript 2.0: Control Flow Based Type Analysis
  3. TypeScript 2.0: Acquiring Type Declaration Files
  4. TypeScript 2.0: Read-Only Properties
  5. TypeScript 2.0: Tagged Union Types
  6. TypeScript 2.0: More Literal Types
  7. TypeScript 2.0: The never Type
  8. TypeScript 2.0: Built-In Type Declarations
  9. TypeScript 2.1: async/await for ES3/ES5
  10. TypeScript 2.1: External Helpers Library
  11. TypeScript 2.1: Object Rest and Spread
  12. TypeScript 2.1: keyof and Lookup Types
  13. TypeScript 2.1: Mapped Types
  14. TypeScript 2.1: Improved Inference for Literal Types
  15. TypeScript 2.1: Literal Type Widening
  16. TypeScript 2.1: Untyped Imports
  17. TypeScript 2.2: The object Type
  18. TypeScript 2.2: Dotted Properties and String Index Signatures
  19. TypeScript 2.2: Null-Checking for Expression Operands
  20. TypeScript 2.2: Mixin Classes
  21. TypeScript 2.3: Generic Parameter Defaults
  22. TypeScript 2.3: The --strict Compiler Option
  23. TypeScript 2.3: Type-Checking JavaScript Files with --checkJs
  24. TypeScript 2.3: Downlevel Iteration for ES3/ES5
  25. TypeScript 2.4: String Enums
  26. TypeScript 2.4: Weak Type Detection
  27. TypeScript 2.4: Spelling Correction
  28. TypeScript 2.4: Dynamic import() Expressions
  29. TypeScript 2.5: Optional catch Binding
  30. TypeScript 2.6: JSX Fragment Syntax
  31. TypeScript 2.7: Numeric Separators
  32. TypeScript 2.7: Strict Property Initialization
  33. TypeScript 2.8: Per-File JSX Factories
  34. TypeScript 2.8: Conditional Types
  35. TypeScript 2.8: Mapped Type Modifiers