Skip to content

Overly-narrowed generic object loses typing #57804

Closed as not planned
Closed as not planned
@nmussy

Description

@nmussy

🔎 Search Terms

"generic narrowing", "union narrowing", "generic guard", "ts2345"

🕗 Version & Regression Information

  • This is the behavior in every version I tried (from v3.9 to v5.5.0-dev.20240316), and I reviewed the FAQ for entries about type narrowing
  • I was unable to test this on prior versions because Property 'values' does not exist on type 'ObjectConstructor'.(2339)

⏯ Playground Link

https://www.typescriptlang.org/play?ts=5.5.0-dev.20240316#code/MYGwhgzhAEDCD2BbR8B20DeBfAUKSMAgtAKYAeALiagCYwLJqY4CQADgK4BGIAlsNABOJMDTQgAntDaD4bYgF5oAImUBuHLnxRoAIVKVqdOEhToMrTj35CRY1JOmy2+pao24cFCWxInGqACqFLwgAGKoEAA8ACoGVLT0pmgAfNBKFiy8EITwgroAXNAAFABuYCBFDGYAlOlp5SDQ2dAxGiwA5iQUAArORWUVRTF1CmkYTnJFEBSCvKgd0FgeGnhoM9AcISAwGayERZlZOXmFJaUkghC8aFXJqDVFF1c36C2KaQCEn2WX10yQaCEGoAOhkckIABpWJ1un0puc-q8isD6iUJuC2E8kWgwc5iFgatCWFhoBAwCEIAAzXgkJIBYKhCLRQgpYlnI7ZXL5AbPf6oO4BR7QPmvZowVxfH6igES0GY3TE2G9fqIl63PSjNLFDGqmWoPFyfSE4mk8mUml0-xmRnhSJRXRszSrTLAdYUaBdFVydLnIbWtBa5gsFhUvIlN2RD1bULQeBU6AAeS4ACsSMAKCDGhw6cUYzsanUjlkE3ntiCuadBiBC7YKBxBOh8yCvfC2NWau0STDhPXG9BUBwQCB2stNDgcK73Z64c5fdXBWZIZtthBF2hbcyotVUgBtAC6QaOYcEEen+bjCfzEDqvFLzcr+Q7dV7Dab5dbzg7Xdf-cHw9HDwJynKMZ29Nh50addUGXa9oM3e1iAAHz0FIDyPGETzPUCL3jFdQhvZp73LR9dGfOs33wkAW1nORvx7bpKP-EdWDHXAgA

💻 Code

class Common {}
class A extends Common {
	public readonly propA = "";
}
class B extends Common {
	public readonly propB = "";
}

type CommonUtilFns<T extends Common> = {
	isAorB: (val: Common) => val is T;
	getProp: (val: T) => { prop: string };
};

const utils = {
	A: {
		isAorB: (version: Common): version is A => !!(version as A).propA,
		getProp: (version: A) => ({ prop: version.propA }),
	} satisfies CommonUtilFns<A>,
	B: {
		isAorB: (version: Common): version is B => !!(version as B).propB,
		getProp: (version: B) => ({ prop: version.propB }),
	} satisfies CommonUtilFns<B>,
};

const getProp = (val: Common) => {
	for (const util of Object.values(utils)) {
		if (util.isAorB(val)) return util.getProp(val);
	}
	return null;
};

🙁 Actual behavior

Invoking util.getProp after guarding it with util.isAorB results in a type error:

Argument of type 'A | B' is not assignable to parameter of type 'A & B'.
  Type 'A' is not assignable to type 'A & B'.
    Property 'propB' is missing in type 'A' but required in type 'B'.(2345)

The return value of Object.values seems to be correctly typed, as this is the evaluated typing of util:

const util: {
    isAorB: (version: Common) => version is A;
    getProp: (version: A) => {
        prop: string;
    };
} | {
    isAorB: (version: Common) => version is B;
    getProp: (version: B) => {
        prop: string;
    };
}

However, once val is guarded, its type becomes A | B

🙂 Expected behavior

I would expect the type guard to correctly narrow the type as A | B, given I'm using the same instance of CommonUtilFns with the same generic.

This however works correctly with these examples:

const getProp = (val: Common, utils: CommonUtilFns<Common>[]) => {
	for (const util of utils) if (util.isAorB(val)) return util.getProp(val);
	return null;
};
const getProp = (val: Common, utils: CommonUtilFns<A | B>[]) => {
	for (const util of utils) if (util.isAorB(val)) return util.getProp(val);
	return null;
};

Additional information about the issue

This might be related to #17713, but I haven't found another issue specifically addressing this pattern, apologies if I missed it

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions