Skip to content

Let primitive types discriminate a union of objects #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27281,16 +27281,24 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return false;
}

function isDiscriminantProperty(type: Type | undefined, name: __String) {
function isDiscriminantProperty(type: Type | undefined, name: __String, considerNonUniformPrimitivePropDiscriminant: boolean = true) {
if (type && type.flags & TypeFlags.Union) {
const prop = getUnionOrIntersectionProperty(type as UnionType, name);
if (prop && getCheckFlags(prop) & CheckFlags.SyntheticProperty) {
const propType = getTypeOfSymbol(prop);
// NOTE: cast to TransientSymbol should be safe because only TransientSymbols can have CheckFlags.SyntheticProperty
if ((prop as TransientSymbol).links.isDiscriminantProperty === undefined) {
(prop as TransientSymbol).links.isDiscriminantProperty = ((prop as TransientSymbol).links.checkFlags & CheckFlags.Discriminant) === CheckFlags.Discriminant &&
!isGenericType(getTypeOfSymbol(prop));
(prop as TransientSymbol).links.isDiscriminantProperty = new Map();
}
return !!(prop as TransientSymbol).links.isDiscriminantProperty;

if (!(prop as TransientSymbol).links.isDiscriminantProperty!.has(considerNonUniformPrimitivePropDiscriminant)) {
const isDiscriminant = ((((prop as TransientSymbol).links.checkFlags & CheckFlags.Discriminant) === CheckFlags.Discriminant)
|| !!(considerNonUniformPrimitivePropDiscriminant && ((prop as TransientSymbol).links.checkFlags & CheckFlags.HasNonUniformType) && someType(propType, t => !!(t.flags & TypeFlags.Primitive))))
&& !isGenericType(propType);
(prop as TransientSymbol).links.isDiscriminantProperty!.set(considerNonUniformPrimitivePropDiscriminant, isDiscriminant);
}

return !!(prop as TransientSymbol).links.isDiscriminantProperty!.get(considerNonUniformPrimitivePropDiscriminant);
}
}
return false;
Expand All @@ -27299,7 +27307,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function findDiscriminantProperties(sourceProperties: Symbol[], target: Type): Symbol[] | undefined {
let result: Symbol[] | undefined;
for (const sourceProperty of sourceProperties) {
if (isDiscriminantProperty(target, sourceProperty.escapedName)) {
if (isDiscriminantProperty(target, sourceProperty.escapedName, /*considerNonUniformPrimitivePropDiscriminant*/ false)) {
if (result) {
result.push(sourceProperty);
continue;
Expand Down
4 changes: 2 additions & 2 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2756,7 +2756,7 @@ export interface ArrowFunction extends Expression, FunctionLikeDeclarationBase,
readonly modifiers?: NodeArray<Modifier>;
readonly equalsGreaterThanToken: EqualsGreaterThanToken;
readonly body: ConciseBody;
readonly name: never;
readonly name?: never;
}

// The text property of a LiteralExpression stores the interpreted value of the literal in text form. For a StringLiteral,
Expand Down Expand Up @@ -6020,7 +6020,7 @@ export interface SymbolLinks {
leftSpread?: Symbol; // Left source for synthetic spread property
rightSpread?: Symbol; // Right source for synthetic spread property
syntheticOrigin?: Symbol; // For a property on a mapped or spread type, points back to the original property
isDiscriminantProperty?: boolean; // True if discriminant synthetic property
isDiscriminantProperty?: Map<boolean, boolean | undefined> // Key is a flag, value is true if discriminant synthetic property
resolvedExports?: SymbolTable; // Resolved exports of module or combined early- and late-bound static members of a class.
resolvedMembers?: SymbolTable; // Combined early- and late-bound members of a symbol
exportsChecked?: boolean; // True if exports of external module have been checked
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4989,7 +4989,7 @@ declare namespace ts {
readonly modifiers?: NodeArray<Modifier>;
readonly equalsGreaterThanToken: EqualsGreaterThanToken;
readonly body: ConciseBody;
readonly name: never;
readonly name?: never;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is basically name?: undefined without exactOptionalPropertyTypes (and TS codebase doesnt use this flag). Is this property truly optional at runtime anyway? or is it always set to undefined?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems that createArrowFunction do not set it at all

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk if it is set to undefined somewhere else, I'd say it isn't. Even updateArrowFunction ignores it

}
interface LiteralLikeNode extends Node {
text: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
narrowUnionOfObjectsByPrimitiveProperty.ts(35,20): error TS2550: Property 'repeat' does not exist on type 'string'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later.
narrowUnionOfObjectsByPrimitiveProperty.ts(55,13): error TS2550: Property 'repeat' does not exist on type 'string'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later.
narrowUnionOfObjectsByPrimitiveProperty.ts(60,1): error TS2322: Type '{ prop: string | number; other: never; }' is not assignable to type '{ prop: string; other: string; } | { prop: number; other: number; }'.
Type '{ prop: string | number; other: never; }' is not assignable to type '{ prop: number; other: number; }'.
Types of property 'prop' are incompatible.
Type 'string | number' is not assignable to type 'number'.
Type 'string' is not assignable to type 'number'.
narrowUnionOfObjectsByPrimitiveProperty.ts(68,1): error TS2322: Type '{ prop: string | number; other: never; }' is not assignable to type '{ prop: string; other: string; } | { prop: number; other: number; }'.
Type '{ prop: string | number; other: never; }' is not assignable to type '{ prop: number; other: number; }'.
Types of property 'prop' are incompatible.
Type 'string | number' is not assignable to type 'number'.
Type 'string' is not assignable to type 'number'.
narrowUnionOfObjectsByPrimitiveProperty.ts(71,14): error TS2550: Property 'repeat' does not exist on type 'string'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later.


==== narrowUnionOfObjectsByPrimitiveProperty.ts (5 errors) ====
interface State<Type> {
state: Type;
}

interface UserName {
first: string;
last?: string;
}

const nameState = {} as {
value: string;
state: State<string>;
} | {
value: UserName;
state: State<UserName>;
}

if (typeof nameState.value === "string") {
nameState.state satisfies State<string>;
} else {
nameState.state satisfies State<UserName>;
}


declare const arr: [string, number] | [number, string];
if (typeof arr[0] === "string") {
arr[1] satisfies number;
} else {
arr[1] satisfies string;
}


function aStringOrANumber<T extends { a: string } | { a: number }>(param: T): T extends { a: string } ? string : T extends { a: number } ? number : never {
if (typeof param.a === "string") {
return param.a.repeat(3);
~~~~~~
!!! error TS2550: Property 'repeat' does not exist on type 'string'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later.
}
if (typeof param.a === "number") {
return Math.exp(param.a);
}
throw new Error()
}

aStringOrANumber({ a: "string" })
aStringOrANumber({ a: 42 })


// The following two tests ensure that the discriminativeness of property 'prop'
// is treated differently in assignability and narrowing, and that the discriminativeness is properly cached.
declare let obj: { prop: string, other: string } | { prop: number, other: number }

// Here, we first perform narrowing, but the subsequent assignability should not be affected.
// We expect an error there because of an incorrect value assigned to 'prop'.
// See contextualTypeWithUnionTypeObjectLiteral.ts
if(typeof obj.prop === "string") {
obj.other.repeat(3);
~~~~~~
!!! error TS2550: Property 'repeat' does not exist on type 'string'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later.
} else {
Math.exp(obj.other);
}

obj = { prop: Math.random() > 0.5 ? "whatever" : 42, other: "irrelevant" as never }
~~~
!!! error TS2322: Type '{ prop: string | number; other: never; }' is not assignable to type '{ prop: string; other: string; } | { prop: number; other: number; }'.
!!! error TS2322: Type '{ prop: string | number; other: never; }' is not assignable to type '{ prop: number; other: number; }'.
!!! error TS2322: Types of property 'prop' are incompatible.
!!! error TS2322: Type 'string | number' is not assignable to type 'number'.
!!! error TS2322: Type 'string' is not assignable to type 'number'.


declare let obj2: { prop: string, other: string } | { prop: number, other: number }

// Here, we first assign a value to 'obj2' and then perform narrowing.
// We expect an error here because of an incorrect value assigned to 'prop', like above,
// but the subsequent narrowing should not be affected by the assignability.
obj2 = { prop: Math.random() > 0.5 ? "whatever" : 42, other: "irrelevant" as never }
~~~~
!!! error TS2322: Type '{ prop: string | number; other: never; }' is not assignable to type '{ prop: string; other: string; } | { prop: number; other: number; }'.
!!! error TS2322: Type '{ prop: string | number; other: never; }' is not assignable to type '{ prop: number; other: number; }'.
!!! error TS2322: Types of property 'prop' are incompatible.
!!! error TS2322: Type 'string | number' is not assignable to type 'number'.
!!! error TS2322: Type 'string' is not assignable to type 'number'.

if(typeof obj2.prop === "string") {
obj2.other.repeat(3);
~~~~~~
!!! error TS2550: Property 'repeat' does not exist on type 'string'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later.
} else {
Math.exp(obj2.other);
}
124 changes: 124 additions & 0 deletions tests/baselines/reference/narrowUnionOfObjectsByPrimitiveProperty.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//// [tests/cases/compiler/narrowUnionOfObjectsByPrimitiveProperty.ts] ////

//// [narrowUnionOfObjectsByPrimitiveProperty.ts]
interface State<Type> {
state: Type;
}

interface UserName {
first: string;
last?: string;
}

const nameState = {} as {
value: string;
state: State<string>;
} | {
value: UserName;
state: State<UserName>;
}

if (typeof nameState.value === "string") {
nameState.state satisfies State<string>;
} else {
nameState.state satisfies State<UserName>;
}


declare const arr: [string, number] | [number, string];
if (typeof arr[0] === "string") {
arr[1] satisfies number;
} else {
arr[1] satisfies string;
}


function aStringOrANumber<T extends { a: string } | { a: number }>(param: T): T extends { a: string } ? string : T extends { a: number } ? number : never {
if (typeof param.a === "string") {
return param.a.repeat(3);
}
if (typeof param.a === "number") {
return Math.exp(param.a);
}
throw new Error()
}

aStringOrANumber({ a: "string" })
aStringOrANumber({ a: 42 })


// The following two tests ensure that the discriminativeness of property 'prop'
// is treated differently in assignability and narrowing, and that the discriminativeness is properly cached.
declare let obj: { prop: string, other: string } | { prop: number, other: number }

// Here, we first perform narrowing, but the subsequent assignability should not be affected.
// We expect an error there because of an incorrect value assigned to 'prop'.
// See contextualTypeWithUnionTypeObjectLiteral.ts
if(typeof obj.prop === "string") {
obj.other.repeat(3);
} else {
Math.exp(obj.other);
}

obj = { prop: Math.random() > 0.5 ? "whatever" : 42, other: "irrelevant" as never }


declare let obj2: { prop: string, other: string } | { prop: number, other: number }

// Here, we first assign a value to 'obj2' and then perform narrowing.
// We expect an error here because of an incorrect value assigned to 'prop', like above,
// but the subsequent narrowing should not be affected by the assignability.
obj2 = { prop: Math.random() > 0.5 ? "whatever" : 42, other: "irrelevant" as never }

if(typeof obj2.prop === "string") {
obj2.other.repeat(3);
} else {
Math.exp(obj2.other);
}

//// [narrowUnionOfObjectsByPrimitiveProperty.js]
"use strict";
var nameState = {};
if (typeof nameState.value === "string") {
nameState.state;
}
else {
nameState.state;
}
if (typeof arr[0] === "string") {
arr[1];
}
else {
arr[1];
}
function aStringOrANumber(param) {
if (typeof param.a === "string") {
return param.a.repeat(3);
}
if (typeof param.a === "number") {
return Math.exp(param.a);
}
throw new Error();
}
aStringOrANumber({ a: "string" });
aStringOrANumber({ a: 42 });
// Here, we first perform narrowing, but the subsequent assignability should not be affected.
// We expect an error there because of an incorrect value assigned to 'prop'.
// See contextualTypeWithUnionTypeObjectLiteral.ts
if (typeof obj.prop === "string") {
obj.other.repeat(3);
}
else {
Math.exp(obj.other);
}
obj = { prop: Math.random() > 0.5 ? "whatever" : 42, other: "irrelevant" };
// Here, we first assign a value to 'obj2' and then perform narrowing.
// We expect an error here because of an incorrect value assigned to 'prop', like above,
// but the subsequent narrowing should not be affected by the assignability.
obj2 = { prop: Math.random() > 0.5 ? "whatever" : 42, other: "irrelevant" };
if (typeof obj2.prop === "string") {
obj2.other.repeat(3);
}
else {
Math.exp(obj2.other);
}
Loading
Loading