Typescript: Type parameter explicitly constrained by `unknown` should not be assignable to `{}`

Created on 31 Aug 2018  路  5Comments  路  Source: microsoft/TypeScript


TypeScript Version: master (2deb318)


Search Terms: conditional type variable parameter undefined null empty object constraint

Code

// With --strictNullChecks
type Test1 = [unknown] extends [{}] ? true : false;  // false
type IsDefinitelyDefined<T extends unknown> = [T] extends [{}] ? true : false;
type Test2 = IsDefinitelyDefined<unknown>;  // true, should be false

function oops<T extends unknown>(arg: T): {} {
    return arg;  // no error, should be an error (unknown not assignable to {})
}
console.log(oops(null).constructor);  // bang!

Expected behavior / Actual behavior: as marked

Playground Link: link (remember to enable strictNullChecks)

Related Issues: didn't find any

Discovered via https://stackoverflow.com/questions/52105268 .

Committed Suggestion

Most helpful comment

I would happily take the breaking change (under a new strict flag) for the soundness; in fact, I assumed it had come as part of --strictNullChecks until I discovered this issue. (What was the specific reason that the change was unacceptable to make as part of --strictNullChecks, which was a large breaking change already?)

If you don't want to do that, we could at least make a type parameter explicitly constrained by unknown not assignable to {}; nobody should be relying on that behavior. If we do that and then people use a lint rule to require every type parameter in their code to have a constraint (either unknown, {}, or something else), then we'll be in pretty good shape; the standard library may still have legacy unconstrained type parameters, but it seems unlikely to me that any of them will expose the buggy behavior. Shall I write the PR for the assignability change?

All 5 comments

In the early days before --strictNullChecks, the {} type was our top type. With --strictNullChecks we carved out null and undefined and effectively our top type then became {} | null | undefined. However, for reasons of backward compatibility we kept {} as the default inference for an unconstrained type parameter and we kept the rule that allows an unconstrained type parameter to be assigned to {}.

We now have a proper top type called unknown and if it wasn't for backward compatibility we'd switch from {} to unknown in the situations above. However, it would be a significant breaking change.

I would happily take the breaking change (under a new strict flag) for the soundness; in fact, I assumed it had come as part of --strictNullChecks until I discovered this issue. (What was the specific reason that the change was unacceptable to make as part of --strictNullChecks, which was a large breaking change already?)

If you don't want to do that, we could at least make a type parameter explicitly constrained by unknown not assignable to {}; nobody should be relying on that behavior. If we do that and then people use a lint rule to require every type parameter in their code to have a constraint (either unknown, {}, or something else), then we'll be in pretty good shape; the standard library may still have legacy unconstrained type parameters, but it seems unlikely to me that any of them will expose the buggy behavior. Shall I write the PR for the assignability change?

Per notes in #26954, committed to trying it, at least

The original issue here is actually not controversial. A type parameter with an _explicit_ constraint of unknown shouldn't be assignable to {}. We should just fix that.

The larger issue is whether it is possible for us to switch the default constraint for type parameters to unknown as we discussed in #26954. We should run that experiment and discuss.

So, is it planned for next release ?

Was this page helpful?
0 / 5 - 0 ratings