admin管理员组

文章数量:1384375

function dostuff(tag: any) {
  if (tag in primitives) return primitives[tag]
  // ...

If the condition is true, tag still has the type any.

Is there a way to make this work according to in operator narrowing?

function dostuff(tag: any) {
  if (tag in primitives) return primitives[tag]
  // ...

If the condition is true, tag still has the type any.

Is there a way to make this work according to in operator narrowing?

Share Improve this question asked Mar 18 at 1:45 user29889977user29889977 1532 silver badges8 bronze badges 4
  • Please edit your code to be a minimal reproducible example we can copy and paste into our own IDEs to see what you're seeing. At a minimum you need to define primitives. Also, could you explain why you'd expect tag to be narrowed to something? What would it be narrowed to? The documentation you've linked to says that it would narrow primitives, not tag. Narrowing of tag might be ms/TS#43284 but it's not part of the language because it's not considered correct to do it. Please edit to clarify. – jcalz Commented Mar 18 at 2:05
  • Yeah that issue is the thing I was trying to do. Guess it's not valid TS yet. If you post that comment as an answer I'll mark it as accepted. – user29889977 Commented Mar 18 at 2:23
  • Apparently you accepted another answer before I got here. What should I do here? The accepted answer doesn't link to any authoritative sources, just my comment which could be deleted. I could write up an answer if you still want it, but I'm not sure how to proceed. – jcalz Commented Mar 18 at 13:47
  • @jcalz I unaccepted his. Go for it. – user29889977 Commented Mar 18 at 13:55
Add a comment  | 

2 Answers 2

Reset to default 2

Currently, in operator narrowing only narrows the type of the second operand. So if you check key in obj, the type of obj might be narrowed, but key will be unaffected. People generally use in operator narrowing to filter a union-typed object.


There is an open feature request at microsoft/TypeScript#43284 to allow in to also narrow the first operand, so that key in obj will cause key to be assignable to keyof typeof obj. It's unlikely that this will ever be implemented, seeing as a virtually identical feature request at microsoft/TypeScript#48149 was declined.

The main problem with narrowing key to be keyof typeof obj is that TypeScript object types are not sealed or "exact" (to use terminology from Flow). If obj is of type {a: string}, that means it has a string-valued a property, but it does not mean that it has only that property. It might well have a lot of other properties that TypeScript doesn't know about. TypeScript object types are open, extendible, or "inexact". That allows you to extend interfaces and add properties while maintaining compatibility with the extended type. There's a longstanding open feature request at microsoft/TypeScript#12936 to support exact types. If that were ever implemented, then if obj were of an exact type, key in obj really would be enough to narrow key. But as it stands now, if obj has more properties than TS knows about, then key in obj doesn't really give you any reason to believe that key happens to be in the set of keys TypeScript does know about. Here's an example of the kind of problem you run into:

interface Foo {
    x: string,
    y: string,
}

function process(obj: Foo, key: string) {
    if (key in obj) { 
        // imagine that key is narrowed to 
        console.log(obj[key].toUpperCase())
    }
}

That looks like it might be fine, right? The key in obj check narrows key from string to keyof Foo, so that obj[key] is known to be string, and thus has a toUpperCase() method. But then:

interface Bar extends Foo {
    a: boolean,
    b: number,
    c: Date
}

const bar: Bar = { x: "abc", y: "def", a: true, b: 123, c: new Date() };
process(bar, "b"); // <-- RUNTIME ERROR!
// obj[key].toUpperCase is not a function

TypeScript will allow you to call process(bar, "b"), because bar is a Bar, which is a Foo, because TypeScript types are open and not sealed. And then we see the problem inside process. If TypeScript assumes that "b" in bar implies that "b" is actually "x" or "y", that's a problem, and you get a runtime error, because obj.b is not a string, so it has no toUpperCase() method. It's therefore not sound or type safe to allow such narrowing.


So that's the stated reason why they're unlikely to implement it. Of course the current in-operator narrowing is also unsafe in a similar way, for the same reason:

function processTwo(obj: { a: string } | { b: string }) {
    if ("a" in obj) {
        console.log(obj.a.toUpperCase())
    } else {
        console.log(obj.b.toUpperCase())
    }
}

const obj = { a: 123, b: "abc" };
processTwo(obj); // <-- RUNTIME ERROR! 
// obj.a.toUpperCase is not a function

So the soundness issue is already present, and they allow it because this kind of thing doesn't happen very often. When people use in operator narrowing, they assume types are exact.

In my mind the only problem with supporting key narrowing is determining how it should interact with object narrowing. If you check key in obj it certainly shouldn't narrow both key and obj, right? See this comment on microsoft/TypeScript#42384. Perhaps there's some heuristic, like... if key is wide like string, or a union, then we narrow key, but if key is some known string literal like "a" then we narrow obj. It's not obvious how to do this that wouldn't break existing code, though.


Anyway, it's unlikely to happen. So if you need this functionality, you'll have to implement it yourself. You can do so by wrapping the in check in a user-defined type guard function like:

function inOperator<T extends object>(key: any, obj: T): key is keyof T {
    return key in obj;
}

which enables the process() function above to be written like

function process(obj: Foo, key: string) {
    if (inOperator(key, obj)) {
        console.log(obj[key].toUpperCase())
    }
}

That compiles, and key is narrowed to keyof Foo, so obj[key] is allowed and seen to be of type string. Of course that still has the problem of unsoundness, so just... don't call process with an obj which has more properties than Foo does, okay?

Playground link to code

The in operator narrows the right-hand-side of the in, so in your code snippet, it can narrow primitives, and not tag within your if statement. Furthermore, tag must be compared to a known key of primitives to successfully narrow primitives within the conditional.

If your intention was to narrow narrow tag within your if statement, then as jcalz's comment to your question explains, that's not possible/desirable.

本文标签: Narrowing for TypeScript quotinquot operator with arbitrary keyStack Overflow