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!