In TypeScript generics, what does each of the constraints `T extends object`, `T extends {}` and `T extends unknown` actually allow a caller to pass?
answer
- two of the three names mislead
- non-primitive versus non-nullish
- {} is not "empty object"
- NonNullable is defined as T & {}
- one of them filters nothing at all
basics
~20 sobject means non-primitive, so arrays, functions and class instances pass while strings and numbers are rejected. {} means anything except null and undefined, so primitives pass. unknown constrains nothing at all — it is the same set as an unconstrained parameter.
solid answer
~50 sThey are three different sets, and the names mislead. `object` is the *non-primitive* type: arrays, functions, class instances and object literals are assignable, but `f("hi")` fails with `Argument of type 'string' is not assignable to parameter of type 'object'`. `{}` is not "an empty object" — it means *any value with no null-ness*, so `42` and `"hi"` pass and only `null` and `undefined` are rejected under `strictNullChecks`; that is exactly why `NonNullable<T>` is defined in the standard library as `T & {}`. `T extends unknown` accepts everything, including `null` and `undefined`, so as a filter it is a no-op identical to a bare `<T>`; people write it mainly to keep a generic arrow function from being parsed as JSX in a .tsx file. Note that without `strictNullChecks` the `{}` distinction evaporates, because `null` and `undefined` are then assignable to everything.
code
typescript · 15 linesdeclare function nonPrimitive<T extends object>(x: T): T;
declare function notNullish<T extends {}>(x: T): T;
declare function anything<T extends unknown>(x: T): T;
nonPrimitive([1, 2]);
nonPrimitive(() => {});
nonPrimitive(new Date());
// nonPrimitive("hi"); // TS2345: 'string' is not assignable to 'object'
notNullish(42);
notNullish("hi");
// notNullish(null); // TS2345: 'null' is not assignable to '{}'
anything(null);
anything(undefined);go deeper
Be able to state the one-line meaning of each: object excludes primitives, {} excludes only null and undefined, unknown excludes nothing. Do not read {} as "an empty object".
Explain why primitives satisfy {} in assignability terms, order the three from most to least restrictive, and cite NonNullable<T> = T & {} as the library's own use of the trick.
Say which one you would actually reach for and why a member-shaped constraint usually beats all three, and flag that strictNullChecks changes the answer for {} in a legacy codebase.
Own the guidance that broad constraints are a code smell in a shared API: they accept values your body cannot really handle, so decide as policy where object is genuinely the contract and where a structural shape belongs instead.
## Three constraints, three different sets Candidates routinely rank these as "loose, looser, loosest" and get the order wrong, because two of the three names suggest something other than what they mean. ### `T extends object` — non-primitive `object` is the type of everything that is *not* a primitive. Primitives are `string`, `number`, `bigint`, `boolean`, `symbol`, `null` and `undefined`; everything else — arrays, tuples, functions, class instances, plain object literals — is assignable. ```ts declare function f<T extends object>(x: T): T; f([1, 2]); // ok f(() => {}); // ok f(new Date()); // ok // f("hi"); // Error TS2345: Argument of type 'string' is not assignable to parameter of type 'object'. ``` Use it when the body does something only reference values support — spreading, `Object.keys`, storing in a `WeakMap`. ### `T extends {}` — anything but `null` and `undefined` The empty object type is not "an object with no properties". Assignability asks *does the source have everything the target requires*, and `{}` requires nothing, so almost every type qualifies — including primitives, which are assignable because they have the members of their wrapper types. ```ts declare function g<T extends {}>(x: T): T; g(42); // ok g("hi"); // ok g([1, 2]); // ok // g(null); // Error TS2345: Argument of type 'null' is not assignable to parameter of type '{}'. ``` The only values it excludes are `null` and `undefined`, which have no members at all. That is the whole basis of the standard library definition `type NonNullable<T> = T & {}` — intersecting with `{}` strips the null-ish members of a union. ### `T extends unknown` — no constraint at all `unknown` is the top type: every type is assignable to it. As a constraint it filters nothing, so `<T extends unknown>` accepts exactly the same arguments as a bare `<T>`, `null` and `undefined` included. It has one practical use: in a `.tsx` file a generic arrow function `<T>(x: T) => x` is parsed as a JSX element, and writing `<T extends unknown>(x: T) => x` (or the trailing-comma form `<T,>`) disambiguates it. Inside the body it buys nothing — an unconstrained parameter is already treated as `unknown`, so you still cannot read any member off it. ## The ordering, stated once From most to least restrictive: `object` (no primitives) → `{}` (no null-ish) → `unknown` (nothing excluded). The intuitive ranking that puts `{}` tightest is the classic wrong answer. ## `strictNullChecks` changes the picture With `strictNullChecks` off, `null` and `undefined` are assignable to every type, so `T extends {}` stops rejecting them and becomes indistinguishable from `T extends unknown`. Any interview answer about `{}` should state the assumption; in a modern codebase `strict` is on and the distinction is real. ## Choosing between them The honest default is **none of the three**. A constraint should describe the members the body actually uses, so `T extends { id: string }` beats `T extends object` whenever you read `id`. The three broad constraints are for the cases where you genuinely accept anything of a category: - Reach for `object` when the operation is meaningless on primitives — a deep-merge helper, a `WeakMap` cache key, a proxy wrapper. - Reach for `{}` when the only thing you must exclude is null-ness — a helper that asserts presence, or the shape mirrored by `NonNullable`. - Reach for `unknown` only for the `.tsx` parsing workaround, and be explicit that it is a syntax fix, not a type decision. ## Related traps Two further points come up in follow-ups. First, `object` is not the same as `Object`: the capitalised interface is the type of `Object.prototype`'s members, and primitives *are* assignable to it, which makes it a poor constraint. Second, none of this survives compilation — all three constraints are erased, so a value arriving through `any` can be `null` at runtime no matter which one you wrote.
- Why is `{}` a weaker constraint than `object` when it looks more specific?Assignability asks whether the source supplies everything the target requires, and `{}` requires nothing. Primitives such as `42` and `"hi"` therefore satisfy it, while `object` explicitly excludes every primitive. Only `null` and `undefined`, which have no members, fail the `{}` check.
- Is there any reason to write `<T extends unknown>` rather than a bare `<T>`?Only for parsing. In a .tsx file `<T>(x: T) => x` is read as JSX, so `<T extends unknown>` — or the trailing-comma `<T,>` — tells the parser it is a type-parameter list. It changes nothing about which arguments are accepted or what the body may do.
- How does turning off strictNullChecks affect these three constraints?It collapses two of them. Without strictNullChecks, `null` and `undefined` are assignable to every type, so `T extends {}` no longer rejects them and behaves like `T extends unknown`. Only `T extends object` still excludes anything, namely the primitives.
saying these in an interview costs you the question
- Says {} means an object with no properties
- Ranks {} as stricter than object
- Thinks extends unknown restricts the callable set
- Uses Object instead of object as a constraint
- Forgets the answer depends on strictNullChecks