镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Regression: Intersection of array and tuple not assignable to tuple #38348

Description

@NWilson

TypeScript Version: Nightly

Search Terms: tuple intersection array

Expected behavior:
Expected code to compile. It does compile with tsc 3.8, but does not compile with tsc 3.9 (Nightly).

Actual behavior:
In the code below, the following error is reported:

const x: [number, ...number[]]
Type 'number[] & [number, ...number[]]' is not assignable to type '[number, ...number[]]'.(2322)

Code

const y: number[] & [number, ...number[]] = [1];
const x: [number, ...number[]] = y;

Discussion

Presumably, this is related to the breaking change in 3.9 around handling of tuple types more strictly. I believe the code is correct however and should compile, because the array type number[] is a strict supertype of the tuple type [number, ...number[]] (surely?) - perhaps the special case unifying tuple types with matching element types to an array type hasn't been implemented?

Discovered by Jason Gore (@JasonGore) and Nick Wilson (@NWilson) (MSFT).

Output
"use strict";
const y = [1];
const x = y;
Compiler Options
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "useDefineForClassFields": false,
    "alwaysStrict": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "downlevelIteration": false,
    "noEmitHelpers": false,
    "noLib": false,
    "noStrictGenericChecks": false,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "esModuleInterop": true,
    "preserveConstEnums": false,
    "removeComments": false,
    "skipLibCheck": false,
    "checkJs": false,
    "allowJs": false,
    "declaration": true,
    "experimentalDecorators": false,
    "emitDecoratorMetadata": false,
    "target": "ES2017",
    "module": "ESNext"
  }
}

Playground Link: Provided

Activity

  1. JasonGore commented on May 5, 2020

    @JasonGore

    Also applies to 3.9.1-rc.

  2. DanielRosenwasser commented on May 5, 2020

    @DanielRosenwasser
    Member

    I believe that this is because we no longer just consider intersections to be assignable if any constituent is assignable - the type has to be compatible as a whole. Anders Hejlsberg (@ahejlsberg) made that change at #37195

    Is there a specific reason you had a number[] & [number, ...number[]]?

  3. ahejlsberg commented on May 6, 2020

    @ahejlsberg
    Member

    The issue here is that we lack the ability to relate a tuple type with a rest element on the target side to anything but another tuple type on the source side. In the extra check introduced by #37195, the source type is an intersection, and so the check fails. I think an easy and reasonable fix is to omit the extra check when the target is a tuple type with a rest element.

  4. NWilson commented on May 6, 2020

    @NWilson
    Author

    I can see it's correct that number[] won't assign to [number, ...number[]]. But the intersection checking logic should realise that the two types being interescted are not unrelated, but that the tuple is a refinement of the array.

    For example, this works in Nightly:

    const y: { x: 1 } & { x: number } = { x: 1 };
    const x: { x: 1 } = y;

    Even though { x: number } can't be assigned to { x : 1 } the checker doesn't do the thing where it requires both parts of the intersection type to be assignable to the target. So the logic is there correctly for objects; the case I ran into is the corresponding handling for an array+tuple intersection.

  5. NWilson commented on May 6, 2020

    @NWilson
    Author

    Is there a specific reason you had a number[] & [number, ...number[]]?

    Apologies, I didn't motivate this very well! The example was perhaps too minimal.

    The full code looked more like this (reworked to a vaguely minimal example). I hope you can see what was being attempted. No apologies made for the code - it is what it is - and thinking some more I can see that the types could be improved, but it's less clear what's going on in the original code which is a bit more involved.

    type Checker<T> = (x: unknown) => x is T;
    
    type NonEmptyArray<T> = [T, ...T[]];
    
    function bothChecker<T1, T2>(c1: Checker<T1>, c2: (x: T1) => Checker<T2>): Checker<T1 & T2> {
        return (x: unknown): x is T1 & T2 => c1(x) && c2(x)(x);
    }
    
    const numChecker: Checker<number> = (x: unknown): x is number => typeof x === 'number';
    function arrayChecker<T>(elt: Checker<T>): Checker<T[]> {
        return (x: unknown): x is T[] => Array.isArray(x) && x.every(i => elt(i));
    }
    function nonEmptyChecker<T>(): Checker<NonEmptyArray<T>> {
            return (x: unknown): x is NonEmptyArray<T> => (x as T[]).length > 0;
    }
    
    const theProblem: Checker<NonEmptyArray<number>> = bothChecker(
        arrayChecker(numChecker),
        _asArray => nonEmptyChecker()
    );
  6. NWilson commented on May 8, 2020

    @NWilson
    Author

    Wow, thank you so much for a quick fix!

  7. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugA bug in TypeScriptFix AvailableA PR has been opened for this issue

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions