Marius Schulz
Marius Schulz
Front End Engineer

Weak Type Detection in TypeScript

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 using the unknown type to the PrettierConfig type:

interface PrettierConfig {
  [prop: string]: unknown;
  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 article and 44 others are part of the TypeScript Evolution series. Have a look!