skip to content

With `strictNullChecks` on, the TypeScript mapped type `type Concrete<T> = { [K in keyof T]-?: T[K] }` is applied to `{ a?: string }`. What is the type of `a` on the result, and what exactly did `-?` do?

level: middleimportance: should knowfreq 40%

answer

  1. optionality is two things, not one
  2. the marker and the union member
  3. required-but-undefined would be useless
  4. this is how Required is defined
  5. -? sits after the bracket, before the colon

basics

~20 s

The result is a required a of type string, not string | undefined. Under strictNullChecks the minus-question-mark modifier does two things: it drops the optional marker and it removes undefined from the property type it copied over.

solid answer

~40 s

`a` comes out as a required property of type `string`. Under `strictNullChecks`, declaring `a?: string` makes the property's type `string | undefined`, so simply dropping the `?` would leave you with a required `a: string | undefined` — which is worse than useless, because you would then be forced to supply a value that is allowed to be `undefined`. TypeScript treats `-?` as removing both halves of optionality: the marker and the `undefined` the marker implied. That is precisely why `Required<T>` in `lib.es5.d.ts` is defined as `{ [P in keyof T]-?: T[P] }` and still gives you genuinely non-nullable properties. The mirror-image `+?` (or a bare `?`) adds the marker back.

code

typescript · 14 lines
typescript
type Concrete<T> = { [K in keyof T]-?: T[K] };

interface Draft {
  a?: string;
  b?: number;
}

type Filled = Concrete<Draft>; // { a: string; b: number }

const ok: Filled = { a: 'x', b: 1 };
// const missingKey: Filled = { b: 1 };
// Error: Property 'a' is missing
// const explicitUndefined: Filled = { a: undefined, b: 1 };
// Error: Type 'undefined' is not assignable to type 'string'

go deeper

for a junior

Know that a property written with ? may be missing and that its type includes undefined when strict null checking is on; those two facts are the setup for everything else here.

for a middle

Explain that -? strips both the marker and the implied undefined, and connect that to why Required is defined as a mapped type with -? rather than as a wrapper around NonNullable.

for a senior

Point out that the guarantee is compile-time only: data crossing an untyped boundary can still arrive without the key, so pair the type with real validation instead of trusting the modifier.

for a principal

Weigh whether required-everything shapes are the right model at all — often a discriminated union or a parsed-once boundary type expresses the invariant better than mechanically requiring every optional field.

## Two things travel together Under `strictNullChecks`, writing `?` on a property means two separate things at once: 1. the property may be **absent** from the object entirely, and 2. the property's **type** includes `undefined`. ```ts interface Draft { a?: string } // a is optional, and its type is string | undefined ``` Because those two things arrive together, a modifier that only undid the first one would be a trap. Consider what a naive "remove the question mark" would produce: ```ts // hypothetical, NOT what TypeScript does { a: string | undefined } ``` That is a required property you must supply, whose value is allowed to be `undefined` — the worst of both worlds. Every caller is forced to write the key, and no caller is forced to give it a real value. ## What -? actually produces TypeScript removes both halves: ```ts type Concrete<T> = { [K in keyof T]-?: T[K] }; interface Draft { a?: string; b?: number } type Filled = Concrete<Draft>; // { a: string; b: number } const ok: Filled = { a: 'x', b: 1 }; // const bad: Filled = { a: undefined, b: 1 }; // Error: Type 'undefined' is not assignable to type 'string' ``` So `-?` is not a syntactic eraser; it is a semantic "make this definitely present and definitely defined". ## Why the standard library relies on it `lib.es5.d.ts` defines the pair as: ```ts type Partial<T> = { [P in keyof T]?: T[P] }; type Required<T> = { [P in keyof T]-?: T[P] }; ``` The `undefined`-stripping behaviour is the entire reason the second line is worth anything. Without it, `Required` would round-trip a `Partial` into required-but-nullable properties, and the utility would have no practical use. Being able to explain that connection — the modifier's semantics justify the utility's definition — is what an interviewer is listening for. ## The mirror direction Adding the marker back is the `+?` (or bare `?`) direction, and it behaves the way you would expect: the property becomes optional, and under `strictNullChecks` its type gains `undefined` when you read it. So `Concrete<Draft>` and `Draft` are not identical types, but round-tripping the modifier is a coherent pair of operations rather than a lossy one for the presence flag. ## Where the sign goes Syntax matters and is easy to fumble. The optional marker lives between the closing bracket of the key clause and the colon, so the sign lives there too: ```ts { [K in keyof T]-?: T[K] } // remove optionality { [K in keyof T]+?: T[K] } // add optionality (same as a bare ?) ``` Writing `-?` before the bracket, or spelling it `?-`, does not compile. Contrast this with `readonly`, whose sign goes in front of the bracket because that is where `readonly` itself goes. ## The flag matters Everything above about `undefined` presupposes `strictNullChecks`. With that check off, `undefined` is assignable to `string` anyway, so the distinction between `string` and `string | undefined` collapses and the interesting half of `-?` has nothing to express — it just drops the marker. If you are asked this question, saying "assuming `strictNullChecks`" before you answer is a cheap way to show you know which flag the behaviour hangs on. ## And where the removal has nothing to remove As with `-readonly`, the removal only bites when the mapping copied a modifier across in the first place. Mapping over `keyof T` does that. Mapping over a literal key union like `[K in 'a' | 'b']` produces required properties to begin with, so `-?` there is inert rather than wrong. ## Common wrong answers The two you hear most often are "`a` becomes `string | undefined`" (only half the modifier's job) and "`-?` is the same as `NonNullable`" (it is not — `NonNullable` is a separate utility operating on a type, while `-?` is a property modifier that also affects presence). A third is the belief that the result somehow guarantees the value exists at runtime; the type layer is erased, so a value parsed from JSON can still arrive without the key no matter what the mapped type says.

  • How does this behaviour change if `strictNullChecks` is turned off?
    The interesting half disappears. Without `strictNullChecks`, `undefined` is assignable to `string` anyway, so `a?: string` and `a: string` differ only in whether the key must be written. `-?` still drops the marker, but there is no `undefined` in the property type for it to strip, and the resulting type gives you far weaker guarantees than it appears to.
  • Is `-?` the same as wrapping the property type in `NonNullable`?
    No. `NonNullable` operates on a type and removes `null` and `undefined` from it, leaving the property's optionality untouched. `-?` is a property modifier: it makes the property required and, under `strictNullChecks`, removes the `undefined` that optionality implied. Mapping with `NonNullable<T[K]>` alone would leave every property still optional.
  • Does a mapped type with `-?` guarantee the key is actually present on a value at runtime?
    No. The mapped type is erased at compile time, so an object parsed from JSON, built by a loop, or cast with `as` can still lack the key entirely. `-?` changes what the checker demands of code it can see; anything crossing an untyped boundary needs a real runtime check or a validation step.

saying these in an interview costs you the question

  • Says -? leaves the property as string | undefined
  • Thinks -? only deletes the question mark character
  • Confuses -? with the NonNullable utility type
  • Believes the result guarantees the key exists at runtime
  • Forgets the behaviour depends on strictNullChecks

context