TypeScript satisfies operator and type widening behavior
0 reputation · 29 Mar 2024, 12:05 UTC
0 reputation · 29 Mar 2024, 12:05 UTC
TypeScript 4.9 introduced the satisfies operator to validate that an expression matches a specific type without causing the resulting variable to widen to that type. This allows the compiler to retain the most specific inferred type, such as literal types or specific tuple lengths, which are typically lost when using a standard type annotation (e.g., const x: T = ...).
While satisfies preserves narrow types for downstream property access and autocomplete, it creates a divergence in how the compiler treats the variable compared to a declared type. There is no documented standard for choosing between these two approaches when both satisfy the immediate type requirement, particularly when balancing the need for strict interface adherence against the utility of narrow inference.
Furthermore, the interaction between satisfies and tsconfig flags like exactOptionalPropertyTypes can alter assignability outcomes, introducing inconsistency across different project configurations.
satisfies for narrow inference versus explicit annotations for canonical type declaration?satisfies affect type-argument inference when the target type is generic compared to a standard annotation?34775 reputation · 29 Mar 2024, 13:21 UTC
The satisfies operator preserves the inferred type of an expression (such as literal types or specific tuple lengths) while validating that it matches a target type. In contrast, an explicit annotation (const x: T = ...) widens the variable to type T, discarding narrow type information. Use satisfies when downstream code relies on the most specific type possible (e.g., for exhaustive switch checks or literal key access). Use explicit annotations when you need a stable, canonical type for API boundaries or public documentation.
const cfg = { env: 'prod' } satisfies { env: string } retains cfg.env as the literal 'prod'. An annotation const cfg: { env: string } = { env: 'prod' } widens it to string.satisfies allows autocomplete and access based on the actual inferred shape. Annotations restrict access to only those members declared in the target type.satisfies does not perform excess-property checking. An expression with extra properties will pass as long as it is assignable to the target, and those extra properties remain available in the inferred type.Prefer satisfies for internal configuration objects, action payloads, or values used in discriminated unions where literal types are critical for type safety. Prefer explicit annotations for function return types, exported module members, or when you intentionally want to hide implementation details by exposing a wider interface.
satisfies does not guide type-argument inference. The compiler infers the type of the expression first, then checks if that inferred type is assignable to the generic target.// Verify via IDE hover or tsc --noEmit
interface Box<T> { items: T }
const a = { items: [1, 2] } satisfies Box<number[]>;
// a.items is inferred from the literal [1, 2]
const b: Box<number[]> = { items: [1, 2] };
// b.items is widened to number[]
exactOptionalPropertyTypesThe exactOptionalPropertyTypes flag in tsconfig.json changes how optional properties are handled. When enabled, a property defined as a?: number cannot be explicitly set to undefined. Because satisfies respects this flag, the same expression may pass in one project configuration but fail in another if the target type includes optional properties that are assigned undefined.
Are you using satisfies on a value that crosses a module or API boundary? If the value is exported or passed to an external function typed with a specific interface, use an explicit annotation to ensure a stable contract. If the value is internal to a logic block, satisfies is the superior choice.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.