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

intersection of object type with known key and object type with index signature does not intersect the value types #52931

Closed
DetachHead opened this issue Feb 23, 2023 · 7 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@DetachHead
Copy link
Contributor

Bug Report

πŸ”Ž Search Terms

intersection object type index

πŸ•— Version & Regression Information

5.0.0-dev.20230223

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

interface Foo {
    a: string
}

interface Bar {
    [key: string]: 'foo'
}

declare const foo: (Foo & Bar)['a'] // string

πŸ™ Actual behavior

type of foo is string

πŸ™‚ Expected behavior

type of foo should be "foo"

@Andarist
Copy link
Contributor

A concrete property is preferred here so the index signature is simply ignored here. Such intersections have very weird behaviors and are not that well supported by the compiler.

Note that roughly this should be the equivalent of this type:

interface Baz {
    a: string; // Property 'a' of type 'string' is not assignable to 'string' index type '"foo"'.(2411)
    [key: string]: 'foo'
}

But as we can see this isn't even a valid type. I'm not sure what the answer here is. I think that perhaps it would be good to revise how such intersections play together, raise errors early, etc. Within the TS type system what you are trying to create here is invalid - the problem is that TS silently accepts it and tries to deal with it somehow. I wouldn't rely on this behavior anyhow, from my PoV it should be treated as "undefined behavior".

@fatcerberus
Copy link

fatcerberus commented Feb 23, 2023

Yeah, this kind of type isn't really supported, but the proposed behavior (intersecting the index signature with the known properties) would likely be a breaking change to real-world code, since people tend to use these kinds of intersection as a workaround to get a "rest" index signature... which is itself an elephant gun of a footgun but I digress

@Andarist The rationale for why these intersections are allowed to exist was explained by Ryan a while back and IIRC it was something along the lines that type instantiation can't really fail per se--as in, that's not a thing that can even happen in the compiler. Furthermore TS will never produce never in an intersection between two object types because it can't, in general, prove that such types are uninhabited.

@DetachHead
Copy link
Contributor Author

i don't see why such a type should be considered invalid though.

Note that roughly this should be the equivalent of this type:

interface Baz {
    a: string; // Property 'a' of type 'string' is not assignable to 'string' index type '"foo"'.(2411)
    [key: string]: 'foo'
}

i do agree with the current behavior of showing an error here however, as it's likely to be a mistake. but IMO it's less likely to be a mistake when it comes from an intersection of two different types like in my OP.

if it helps, my use case was importing from a JSON file where its type was unfortunately widened due to #32063, but i wanted to cast it to a type without losing information from the type that came with the import.

here's a simplified example, where i'm trying to get the type {a: 'foo'}:

import foo from '../foo.json' // imported type is `{ a: string }`  but the value is actually {a: 'foo'}

interface Foo {
    [key: string]: 'foo'
}

const castedValue = foo as Foo

const a = castedValue['a'] // 'foo' | undefined, because the `a` key from the imported type was lost (assuming noUncheckedIndexedAccess is on)


const intersectedValue = foo as typeof foo & Foo

const b = intersectedValue['a'] // string :(

@fatcerberus
Copy link

As I said, your proposed behavior would be a breaking change in practice, since people regularly use such intersections to make types that when indexed by a string produce the index type, but also have a few disparate properties for metadata or whatever. If you're thinking "...but that's completely unsafe!", then I agree, but nonetheless it happens. See #17867 for which this kind of intersection is the very first workaround mentioned.

@DetachHead
Copy link
Contributor Author

DetachHead commented Feb 24, 2023

if anyone's interested i made my own intersection type as a workaround:

export type Intersection<T, U> = {
    [K in keyof (T | U)]: Intersection<T[K], U[K]>
} & T & U

interface Foo {
    a: string
}

interface Bar {
    [key: string]: 'foo'
}

declare const foo: Intersection<Foo, Bar>['a'] // 'foo'

i haven't tested it very much but it seems to work well for this use case

edit

turns out it's actually more complicated than that, this doesn't work properly with unions (eg. (Foo | number) & Bar). a better version is available in my ts-helpers package:

import { Intersection } from '@detachhead/ts-helpers/dist/types/misc'

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Feb 24, 2023
@typescript-bot
Copy link
Collaborator

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@skeleton1231
Copy link

I am a new TS learner

I try

interface Foo {
    a: string
}

interface Bar {
    [key: string]: 'foo'
}

type TFB = Omit<Foo & Bar, never>;


// This will cause a TypeScript error
const obj1: TFB = {
    a: 'string', // Error: Type 'string' is not assignable to type 'foo'
    b: 'foo'
};

const obj2: TFB = {
    a: 'foo', 
    b: 'foo'
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

6 participants