Skip to content
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

Type guard and assertion do not properly refine union types with optional properties #60979

Open
skrtheboss opened this issue Jan 16, 2025 · 4 comments · May be fixed by #58520
Open

Type guard and assertion do not properly refine union types with optional properties #60979

skrtheboss opened this issue Jan 16, 2025 · 4 comments · May be fixed by #58520
Labels
Duplicate An existing issue was already created

Comments

@skrtheboss
Copy link

skrtheboss commented Jan 16, 2025

🔎 Search Terms

"type guard union types narrowing"
"asserts is not refining type"
"type refinement union types bug"
"dryRun outputPath assertion"
"optional property refinement bug"

🕗 Version & Regression Information

This changed between versions 4.7.4 and 4.8.4.
After bisecting i found that the regression has been introduced in commit: 2c68ded95498dc637acdb00e8349f51832f5a6df.

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.7.3#code/KYDwDg9gTgLgBASwHY2FAZgQwMbDgMQQBtgBnAYRMyQFcxKF8jMBzUuAbwCg5e4pgmACYQkRAJ5whUcQCUaSAPwAuOACMIEKkgDcPPgOGiJcCDRhhzABUwwAFirikYUZCz0BfLlwD0PuAAq4mB4LDSYUEJwAO4I9nAABgikCXDo0PzA6MhucAoIonAwwWRc6ArYMAVIcNh2wNgA1gxMrKQAFPq86MxsqoQkFNp0Lb2kADRcAJSqPW2I7ByZRmKS0nIKqi40wDrLIqum5pYwNvaqzq5ILHAecAA+nPvGazLySrOYRKR4d9x8iHQcHaczYADp1u84AAyaFwACEoNIYLMFmstjsU04XQBRTsUAg0TgSGARIAolACVB2gByAL1OAAWkZqJOZzspjAVUKyUyAEcaAgBFFkFIZIyoAo4ABbCBCPDUKLSmjOJwhbAIdCSex4cpEIhpYh4MAYsE0qZ6AFeAECGA0KA1ba7LheXz+ACymFF5SQlWqeVIuR1RRKcDCESEZQq3Jq0q9SHaWP+fGwolVEC51VI-SNQ0EtHoREYYzgAF5OB49DjNcC6g1mkXWmx2hmY6QpkmcQDU0hVUtIQpxkc0acMbcy5y25bcbw-HAyeAGqghKogiEAMrYVxcpx2MxEKICbIkxKtrOpGAQJ4DpBbSW7YdsjEXFy5O6PftvBSOLDfX5dvg5wAQUqcIiFXEpN23eAj2QMhT0zNNUheIor0-DZbyKe89lZdFzicV9rluSYZ1nfwIGlOIqiIhJ0PeH8vh+W5UmwTAfjBHEvFdOcAFVAxotiflgdhklSAA3NBA0KCAgWDOsmijX0Y1qeomlGNoAHU4jsIDSCEmAOhxJEc0GSh8xGRsxkmGY4EEtADLSEteSWQwDhMG87x2PZXJQ3DR3wy43weJ4fMOG8GL-cdk14GsQTGCEvxqWEESRFFjjwzFsVIvECSJElyUpaBaXpPBmT89lJ39XkBAFIVgBFGp1glKVZXlWykCVFV4FIdVNW1Bk9QNbISDgE17DNC1OJdbw509b1o39FVcjs4TFL9Qo42QLT7F0-SOk7bs03gM80xMsgzOoCzi3mcsOErbxu1UhtrrYbadL0+yOhO3tJpxHs+zFDCh3Ksc7nLb7SGnMj50XSp6ogjctwQHdSD3GgD0yY88ASCGLzQwH3k8h8QYCwibnfa9EoipjrUA-wQLtL4EeAKDkZgrI4PYHHEN7ZDDkvSmMKJnD0v8uwXyucmSLp0xKJgaiblognv0+SKPBYtjgA4rwgA

💻 Code

export interface FilesCleanupCliFlags {
    readonly dryRun?: boolean;
    readonly outputPath?: string;
}

// Type guard with `is` for refining union types
function checkCliFlags(
    flags: FilesCleanupCliFlags,
): flags is { readonly dryRun: true; readonly outputPath: string } | { readonly dryRun?: false } {
    if (flags.dryRun && !flags.outputPath) {
        throw new Error('The --outputPath option is required in dry-run mode and must specify the full file path.');
    }
    return true;
}

// Main function using the type guard
function main() {
    const options: FilesCleanupCliFlags = {};

    if (checkCliFlags(options)) {
        const { dryRun, outputPath } = options;
        // Expected: TypeScript should refine `options` to { dryRun: true; outputPath: string } | { dryRun?: false }
        // Actual: TypeScript refines `options` only to { dryRun: true; outputPath: string },
        // omitting `{ dryRun?: false }` case.
    }
}

// Using `asserts is` version of the check
function checkCliFlagsWithAsserts(
    flags: FilesCleanupCliFlags,
): asserts flags is { readonly dryRun: true; readonly outputPath: string } | { readonly dryRun?: false } {
    if (flags.dryRun && !flags.outputPath) {
        throw new Error('The --outputPath option is required in dry-run mode and must specify the full file path.');
    }
}

// Main function using asserts
function mainWithAsserts() {
    const options: FilesCleanupCliFlags = {};

    checkCliFlagsWithAsserts(options);

    const { dryRun, outputPath } = options;
    // Expected: TypeScript should refine `options` to { dryRun: true; outputPath: string } | { dryRun?: false }
    // Actual: TypeScript refines `options` only to { dryRun: true; outputPath: string },
    // omitting `{ dryRun?: false }` case.
}

🙁 Actual behavior

TypeScript does not refine the type of options correctly after the type guard (checkCliFlags) or assertion (checkCliFlagsWithAsserts)

  • When using checkCliFlags(optins) or checkCliFlagsWithAsserts(options), the expected refinement is:
    { readonly dryRun: true; readonly outputPath: string } | { readonly dryRun?: false }
  • However, TypeScript only refines options to { readonly dryRun: true; readonly outputPath: string }, and the { dryRun?: false } branch is omitted.

🙂 Expected behavior

TypeScript should refine the type of options to:

{ readonly dryRun: true; readonly outputPath: string } | { readonly dryRun?: false }

Additional information about the issue

No response

@Andarist
Copy link
Contributor

Andarist commented Jan 16, 2025

This is being fixed by #60979 #58520

@skrtheboss
Copy link
Author

skrtheboss commented Jan 16, 2025

Thank you for the feedback, @Andarist! I wasn't able to find that specific issue, when I was searching for the bug, but I agree—the problem seems to be exactly the same! 😊

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jan 16, 2025

I think he meant to link #58520; original bug there is #58518

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Jan 16, 2025
@Andarist
Copy link
Contributor

Ah yes, I definitely have wanted link to #58520 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants