Skip to content

Overly-narrowed generic object loses typing #57804

@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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions