This TypeScript error occurs when you try to use the optional operator ('?') in a mapped type key, which is invalid syntax. Mapped types have their own syntax for making properties optional using the '+' and '-' modifiers with the '?' operator. The fix involves using the correct mapped type syntax for optional properties.
The "Cannot use optional operator '?' with mapped type key" error appears when you incorrectly use the JavaScript/TypeScript optional chaining operator ('?') within a mapped type key definition. Mapped types in TypeScript have a specific syntax for transforming property modifiers, and the '?' symbol has a different meaning in this context. In mapped types, the '?' operator is used with '+' or '-' modifiers to add or remove optionality from properties, but it cannot be used directly in the key mapping expression. This error typically occurs when developers try to create conditional or optional keys in mapped types using syntax that works in object types but not in mapped type keys. For example, you might be trying to create a mapped type that conditionally includes properties based on some criteria, but you're using the wrong syntax for expressing optionality within the key mapping part of the type definition.
Mapped types use different syntax for optional properties than regular object types. Learn the correct pattern:
// WRONG - using '?' in mapped type key
type WrongType<T> = {
[K in keyof T]?: T[K]; // Error: Cannot use optional operator '?' with mapped type key
};
// CORRECT - using '+?' modifier to add optionality
type MakeOptional<T> = {
[K in keyof T]+?: T[K]; // Makes all properties optional
};
// CORRECT - using '-?' modifier to remove optionality
type MakeRequired<T> = {
[K in keyof T]-?: T[K]; // Makes all properties required
};
// Example usage:
interface User {
id: number;
name: string;
email?: string;
}
type OptionalUser = MakeOptional<User>;
// Equivalent to: { id?: number; name?: string; email?: string; }
type RequiredUser = MakeRequired<User>;
// Equivalent to: { id: number; name: string; email: string; }The '+?' and '-?' modifiers are specific to mapped types and control property optionality.
If you're trying to use conditional logic in mapped type keys, you need different syntax:
// WRONG - trying to conditionally include keys with '?'
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]?: T[K]; // Error: '?' not allowed here
};
// CORRECT - remove the '?' from the key mapping
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
// CORRECT - add optionality with '+?' modifier if needed
type FilterByTypeOptional<T, U> = {
[K in keyof T as T[K] extends U ? K : never]+?: T[K];
};
// Example: Filter to only string properties
interface Person {
id: number;
name: string;
age: number;
email: string;
}
type StringProps = FilterByType<Person, string>;
// Result: { name: string; email: string; }
type StringPropsOptional = FilterByTypeOptional<Person, string>;
// Result: { name?: string; email?: string; }The 'as' clause (key remapping) and property modifiers are separate parts of mapped type syntax.
For more complex conditional optionality, use conditional types instead of trying to embed logic in mapped type keys:
// WRONG - complex conditional in mapped type key
type ConditionalOptional<T> = {
[K in keyof T as K extends 'id' ? never : K]?: T[K]; // Error: '?' in wrong place
};
// CORRECT - use a conditional type wrapper
type ConditionalOptional<T> = T extends any ? {
[K in keyof T as K extends 'id' ? never : K]?: T[K];
} : never;
// CORRECT - alternative: combine mapped types with Pick/Omit
type WithoutId<T> = Omit<T, 'id'>;
type OptionalWithoutId<T> = {
[K in keyof WithoutId<T>]+?: WithoutId<T>[K];
};
// Example: Make all properties except 'id' optional
interface Product {
id: string;
name: string;
price: number;
description?: string;
}
type ProductWithoutIdOptional = OptionalWithoutId<Product>;
// Result: { name?: string; price?: number; description?: string; }
// Using built-in utility types:
type PartialExceptId<T> = Omit<Partial<T>, 'id'> & Pick<T, 'id'>;
// This makes all properties optional except 'id'Sometimes the simplest solution is to combine existing utility types rather than writing complex mapped types.
If you have working object type syntax and need to convert it to a mapped type, follow these patterns:
// Object type with optional properties
type OptionalProps = {
name?: string;
age?: number;
email?: string;
};
// Equivalent mapped type
type OptionalPropsMapped<T extends { name?: string; age?: number; email?: string }> = {
[K in keyof T]+?: T[K];
};
// If you need to transform property types:
type TransformToOptional<T> = {
[K in keyof T]+?: K extends 'email' ? string | null : T[K];
};
// Example with transformation
interface UserInput {
name: string;
age: number;
email: string;
}
type UserInputOptional = TransformToOptional<UserInput>;
// Result: { name?: string; age?: number; email?: string | null; }
// For making specific properties optional:
type MakeSpecificOptional<T, Keys extends keyof T> = {
[K in keyof T]: K extends Keys ? T[K] | undefined : T[K];
};
// Example: Make only 'email' optional
type UserWithOptionalEmail = MakeSpecificOptional<UserInput, 'email'>;
// Result: { name: string; age: number; email: string | undefined; }Remember: In mapped types, optionality is controlled by the '+?' or '-?' modifiers, not by placing '?' in the key definition.
When combining template literal types with mapped types, be careful with optional syntax:
// WRONG - '?' in template literal type in mapped type
type AddPrefix<T> = {
[K in keyof T as `prefix_${K}`]?: T[K]; // Error
};
// CORRECT - add optionality with modifier
type AddPrefix<T> = {
[K in keyof T as `prefix_${K}`]+?: T[K];
};
// CORRECT - alternative: make non-optional then add prefix
type AddPrefix<T> = {
[K in keyof T as `prefix_${K}`]: T[K];
};
type OptionalAddPrefix<T> = {
[K in keyof T as `prefix_${K}`]+?: T[K];
};
// Example: Add 'db_' prefix to all properties
interface Config {
host: string;
port: number;
password?: string;
}
type DbConfig = AddPrefix<Config>;
// Result: { db_host: string; db_port: number; db_password?: string; }
type DbConfigOptional = OptionalAddPrefix<Config>;
// Result: { db_host?: string; db_port?: number; db_password?: string; }
// For conditional prefixing:
type ConditionalPrefix<T> = {
[K in keyof T as K extends 'password' ? `secure_${K}` : K]: T[K];
};
type SecureConfig = ConditionalPrefix<Config>;
// Result: { host: string; port: number; secure_password?: string; }Template literal types in key remapping must follow the same rules as regular key remapping.
Often, you don't need to write custom mapped types. Use TypeScript's built-in utilities:
// Common utility types for optional properties:
type Partial<T> = { [K in keyof T]+?: T[K]; }; // Make all properties optional
type Required<T> = { [K in keyof T]-?: T[K]; }; // Make all properties required
// Partial for specific keys:
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// Required for specific keys:
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
// Example usage:
interface Settings {
theme: string;
fontSize: number;
darkMode?: boolean;
notifications?: boolean;
}
// Make only notifications optional (others stay as-is)
type ThemeSettings = PartialBy<Settings, 'notifications'>;
// Result: { theme: string; fontSize: number; darkMode?: boolean; notifications?: boolean; }
// Make darkMode required (others stay as-is)
type RequiredDarkMode = RequiredBy<Settings, 'darkMode'>;
// Result: { theme: string; fontSize: number; darkMode: boolean; notifications?: boolean; }
// Combine utilities:
type UserSettings = Partial<Pick<Settings, 'darkMode' | 'notifications'>> &
Required<Pick<Settings, 'theme' | 'fontSize'>>;
// Result: { theme: string; fontSize: number; darkMode?: boolean; notifications?: boolean; }
// For readonly optional properties:
type ReadonlyPartial<T> = { readonly [K in keyof T]+?: T[K]; };Using built-in types or simple combinations often avoids syntax errors and is more maintainable.
### Understanding Mapped Type Modifiers
TypeScript mapped types have special syntax for property modifiers:
// Modifier syntax:
// +readonly - adds readonly modifier
// -readonly - removes readonly modifier
// +? - adds optional modifier
// -? - removes optional modifier
// Default behavior (no modifier specified):
type DefaultMapped<T> = {
[K in keyof T]: T[K];
};
// Preserves existing readonly/optional modifiers
// Explicit modifier removal:
type RemoveModifiers<T> = {
-readonly [K in keyof T]-?: T[K];
};
// Makes all properties mutable and required
// The '+' modifier is optional (can be omitted):
type MakeOptional<T> = {
[K in keyof T]?: T[K]; // Same as '+?'
};
type MakeRequired<T> = {
[K in keyof T]-?: T[K]; // Must include '-'
};### Key Remapping with 'as'
TypeScript 4.1+ allows key remapping in mapped types using the as clause:
type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};
interface Data {
name: string;
age: number;
}
type DataGetters = Getters<Data>;
// Result: { getName: () => string; getAge: () => number; }The key remapping expression (after as) must evaluate to a string literal type. You cannot use the optional operator ('?') in this expression.
### Conditional Key Inclusion/Exclusion
To conditionally include or exclude keys, use the never type in key remapping:
// Include only string keys
type StringKeys<T> = {
[K in keyof T as K extends string ? K : never]: T[K];
};
// Exclude specific keys
type WithoutId<T> = {
[K in keyof T as K extends 'id' ? never : K]: T[K];
};
// Conditional transformation
type OptionalIfString<T> = {
[K in keyof T]: T[K] extends string ? T[K] | undefined : T[K];
};### Performance Considerations
Complex mapped types with conditional logic can slow down TypeScript compilation. For large codebases:
1. Prefer simple mapped types over deeply nested conditionals
2. Use interface inheritance when possible instead of complex mapped types
3. Cache complex types with type Alias = ComplexMappedType<T>
4. Avoid recursive mapped types that cause infinite type expansion
### Common Pitfalls
1. Confusing '?' in index signatures:
// This is valid - index signature with optional values
type StringDictionary = {
[key: string]?: string; // Values are optional, not keys
};
// Different from mapped type syntax2. Mixing mapped types and intersection:
// May cause unexpected optionality
type Mixed = { [K in 'a' | 'b']: number } & { c?: string };
// 'c' is optional, 'a' and 'b' are required3. Literal union keys lose optionality:
type Keys = 'a' | 'b' | 'c';
type FromUnion = { [K in Keys]: number }; // All required
// To make optional: { [K in Keys]+?: number }### Debugging Mapped Types
Use these techniques to understand what your mapped types produce:
// 1. Use hover in IDE to see expanded type
type Test = Partial<{ a: number; b: string }>;
// Hover shows: { a?: number; b?: string; }
// 2. Create test variables
const test: YourMappedType<Example> = {} as any;
// TypeScript will show errors if structure doesn't match
// 3. Use conditional debugging
type Debug<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
// Wraps type to see its full structure
// 4. Check with typeof
const example = { a: 1, b: 'test' };
type ExampleType = typeof example;
type OptionalExample = Partial<ExampleType>;### Related Utility Types
Beyond Partial and Required, TypeScript provides:
- Readonly<T>: Makes all properties readonly
- Pick<T, K>: Selects specific properties
- Omit<T, K>: Removes specific properties
- Record<K, V>: Creates object type with given keys and value type
- Exclude<T, U> / Extract<T, U>: For union type manipulation
These can often replace custom mapped types and avoid syntax errors.
Function expression requires a return type
Function expression requires a return type
Value of type 'string | undefined' is not iterable
How to fix "Value is not iterable" in TypeScript
Type 'undefined' is not assignable to type 'string'
How to fix "Type undefined is not assignable to type string" in TypeScript
Type narrowing from typeof check produces 'never'
How to fix "Type narrowing produces never" in TypeScript
Type parameter 'T' has conflicting constraints
How to fix "Type parameter has conflicting constraints" in TypeScript