Open JoshuaKGoldberg opened 2 years ago
Previously discussed at https://github.com/microsoft/TypeScript/issues/16314
Notes being taken, but some sample code as food for thought.
// Issues With Defensive Nullish Checks
function foo(x: number) {
// Allowed ✔️
if (x === undefined) {
throw new Error("No! x must not be undefined.");
}
// Error? ❌
if (typeof x === "undefined") {
throw new Error("No! x must not be undefined.");
}
// Happens with function args, array accesses, etc.
}
// Issues With Structurally Assignable Apparent Types
function bar(x: {}, y: { toString(): string }) {
// Allowed?
if (typeof x === "number") {
// ...
}
// Allowed?
if (typeof y === "boolean") {
// ...
}
}
I could understand that the primitives - boolean
, number
, string
are narrowed easily, but there is still the issue of defensive programming - eg how do you allow typeof x === 'undefined'
if x
was narrowed to 'number'
?
It's not as clear cut from a type-system POV as x === undefined
. In that case ofc TS can just "allow" this "unnecessary" check because undefined
is a unique type, but 'undefined'
is not at all.
I think there's still unfortunately a non-trivial number of codebases that use typeof x === 'undefined'
checks instead of x === undefined
due to legacy standards, so it's probably not something you could just "ignore".
Perhaps a flag to union 'undefined'
in to the typeof
? But that really does just smell from an API design perspective.
I'm personally of the opinion that your types should match reality - and defensively programming contrary your well-defined types is smelly - but I also understand that not every codebase can or should work that way.
Yeah the structural typing model that TS runs on does make the "object" case a lot more complicated to reason about because an "object" type isn't necessarily an "object".
One could argue that the most "sound" thing to do is to not narrow the type at all - but that would likely be surprising to most people that don't have a good knowledge of TS's type system.
If you instead treated it as an object, then it's being based off an assumption that (at the vast scale of TS) likely wouldn't hold true in some codebase - which means you're surprising a different class of people.
So I guess this doesn't really pass the "feature that won't surprise anyone" test because there are certain things which might be surprising if you don't understand TS's type systems?
I guess this doesn't really pass the "feature that won't surprise anyone" test because there are certain things which might be surprising if you don't understand TS's type systems?
I would go further and say that even if you do understand TS's type system, the results here will still look surprising/buggy unless you really think about it in a defensive way, which is the sort of behavior that people frequently complain about as unintuitive.
We haven't even mentioned yet how "function"
should be in the results for almost any object type, which would be a further point of confusion.
Suggestion
🔍 Search Terms
typeof typeof string literal value redeclare
List of keywords you searched for before creating this issue. Write them down here so that others can find this suggestion more easily and help provide feedback.
✅ Viability Checklist
My suggestion meets these guidelines:
⭐ Suggestion
If a value is known to be a particular primitive type or union of types, such as
string
ornumber | string
,typeof typeof
should give back the exact string literal type, such as"string"
or"number" | "string"
, respectively.In other words: TypeScript should give a more specific type than just
string
iftypeof <value>
would be known to give back one of >=1 string literals.📃 Motivating Example
When a variable is meant to store the result of a
typeof
expression, TypeScript should be able to infer its string literal type, not just that it's astring
primitive.There should be no errors in this code:
Today:
💻 Use Cases
See this variable declaration: https://github.com/typescript-eslint/typescript-eslint/pull/3891/files#diff-8a7d4566dbe9356145e8b86ccefa6d7a164c0b77818beb78d789e1089e9c4bc4R39
Ideally we would want to call it
let memberType: 'noInitializer' | 'number' | 'string' | undefined
, but later ontypeof member.initializer.value
gives backstring
instead of'number' | 'string'
.Edit: this also causes https://github.com/typescript-eslint/typescript-eslint/issues/3453, so perhaps this borders on a bug?