Giter Club home page Giter Club logo

Comments (14)

erik-kallen avatar erik-kallen commented on April 28, 2024 1

Please don't tell me that my stuff does not make sense, you know nothing about my code and you shouldn't pretend to do it.

Our actual types are something like this:

type FrontendEvent = { type: 'click'; data: {} } | { type: 'navigate'; data: {} }
type BackendEvent = { type: string; data: { __nominativeType: true } }
type Event = FrontendEvent | BackendEvent

function track(event: Event) {
  sendToOurTracker(event);
}

and we want calls to succeed for all event types, but no one is actually looking inside the data after we have converted it to an Event.

Can we work around this? Yes we can (for example by saying BackendEvent = { type: string & { __fixTypeScriptIssue: true }}

But still, as I wrote above: It doesn't make sense that there are any types T1 and T2 such that there is any value that is assignable to T1 but not to T1 | T2.

from typescript.

erik-kallen avatar erik-kallen commented on April 28, 2024

I don't think this is true, because if I remove the string case it is suddenly valid. IOW, this code produces no error, even though it would have the same issue with { foo: "bar" | "baz" } not being assignable to { foo: "bar" } | { foo: "baz" }.

type T = { foo: 'bar', data: {} } | { foo: 'baz', data: {} };

function f1(p: 'bar' | 'baz') {
    ok({ foo: p, data: {}}) // This is accepted without issue
}

function ok(t: T) {
}

from typescript.

snarbies avatar snarbies commented on April 28, 2024

{ foo: "bar" | "baz" } does appear to be considered structurally equivalent to { foo: "bar" } | { foo: "baz" }. What of the fact that T.foo is "bar" | "baz" | "string"? I'd imagine the fact that that can simplify to string could muddy the waters.

(T & {foo: 'bar' | 'baz'})['data'] resolves to {} | {x: number}. I don't think that in theory you could exclude the {x: number}. someT.foo could have the value bar or the value baz and still be a { foo: string, data: { x: number } }. This isn't a proper discriminated union.

from typescript.

erik-kallen avatar erik-kallen commented on April 28, 2024

Whether or not it is a "proper" discriminated union, both { foo: 'bar', data: {} and { foo: 'baz', data: {} } are valid values of the type. The reason I think it is a bug rather than a misfeature is that it seems to me the compiler gets confused by the (unrelated) string case.

from typescript.

erik-kallen avatar erik-kallen commented on April 28, 2024

It doesn't really make sense to me that there exist any types T1 and T2 such that there are values that are assingable to T1 but not to T1 | T2

from typescript.

fatcerberus avatar fatcerberus commented on April 28, 2024

@ahejlsberg Remember when I wrote this comment #57231 (comment) saying such types probably exist in the wild? Yeah, someone pick up that phone because I called it.

from typescript.

RyanCavanaugh avatar RyanCavanaugh commented on April 28, 2024

This type doesn't make any sense; there's no way to soundly access { x: number }, so it's the same as

type T = { foo: string, data: { } }

TypeScript doesn't have negated types and efforts to mimic them will, of course, not succeed.

from typescript.

fatcerberus avatar fatcerberus commented on April 28, 2024

Please don't tell me that my stuff does not make sense, you know nothing about my code and you shouldn't pretend to do it.

You wrote code thinking it would work a certain way and it doesn't. This is because the type as written doesn't make sense under the rules of the type system. We have no doubt the code makes sense to you, but it doesn't make sense to TypeScript under the rules of the type system as designed. That's what Ryan is saying.

But still, as I wrote above: It doesn't make sense that there are any types T1 and T2 such that there is any value that is assignable to T1 but not to T1 | T2.

I don't know what you mean by this because there indeed is no such type. In fact that's the root of your problem: { foo: "bar" } is also a legal { foo: string }. so just knowing that typeof p.foo === 'string' isn't enough information to rule out the first two cases, and thus TS won't do so because it would be unsound.

What you'd need for this to work properly is a way to express

type T = { foo: string & not "bar" & not "baz", data: { x: number } };

AKA negated types. #4196

from typescript.

typescript-bot avatar typescript-bot commented on April 28, 2024

This issue has been marked as "Not a Defect" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

from typescript.

erik-kallen avatar erik-kallen commented on April 28, 2024

But still, as I wrote above: It doesn't make sense that there are any types T1 and T2 such that there is any value that is assignable to T1 but not to T1 | T2.

I don't know what you mean by this because there indeed is no such type.

There is:

type T1 = { foo: 'bar', data: {} } | { foo: 'baz', data: {} };
type T2 = { foo: string, data: { x: number } };

let x: 'bar' | 'baz' = (window as any).x;
const x1: T1 = { foo: x, data: {} }; // This is OK
const x2: T1 | T2 = { foo: x, data: {} }; // But this is not. So the literal is assignable to T1 but not to T1 | T2

@RyanCavanaugh this is the reason I reported it. I know what I'm doing is a little fishy, but there is a value that is assignable to a type T1 but not to T1 | T2

from typescript.

snarbies avatar snarbies commented on April 28, 2024

there is a value that is assignable to a type T1 but not to T1 | T2

The problem is the type T1 | T2 is inherently ambiguous. You could use a type assertion before assigning (or any moral equivalent), but where there is otherwise no proper discriminant, Typescript can't distinguish whether your value is a T1 or a T2, and thus can't validate it as one or the other. In order to satisfy the type, you need to satisfy all constituent union members.

from typescript.

erik-kallen avatar erik-kallen commented on April 28, 2024

From an ideal perspective, there is no question about whether the value satisfies T1 | T2, it 100% does. Why? Because it satisfies T1, and all types that satisfy a constraint T1 should (as @RyanCavanaugh agreed with earlier), satisfy the constraint T1 | T2 for all types T2.

Is this worth fixing in TS? I don't know. But it absolutely means that TS deviates from an ideal constraint checker.

from typescript.

snarbies avatar snarbies commented on April 28, 2024

To be clear, this isn't an assignability issue. The value is assignable to the type T1 | T2:

type T1 = { foo: string, data: { x: number } }
type T2 = { foo: 'bar', data: {} } | { foo: 'baz', data: {} }
type T =  T1 | T2 ;

function ok(t: T) { }

declare const p: 'bar' | 'baz'

// Validation of the value
const value: T2 = { foo: p, data: {}};

// Assignability of the value
ok(value);

// Does not validate
const value2: T = { foo: p, data: {}};

Where you get hung up is before assignment... This is a validation issue.

Think like excess property checks, except there is a stronger case for an error here because the value can absolutely be construed as a malformed T1. If you can't distinguish a T1 from a T2, you can't soundly assume that what you have isn't a malformed T1. The problem goes away when Typescript does not have to distinguish between a T1 and a T2 itself. And I'm not making the case that this is the most desirable behavior (typescript sometimes values practicality over soundness), but the logic is coherent.

It's not necessarily uncommon for a distinction represented in the types to correlate to another distinction not represented in the types (e.g. brand types). Accepting a value where you can't validly rule out a malformed T1 means an invalid value could move through your plumbing and back out and then violate an invariant of external code.

Is it practical? I don't know. I see the utility of the type you're trying to portray here, but there is still a possible logic error being identified, and a very subtle one at that, so while I might be irritated too if I were bit by this the way you were, ultimately I think this is the desirable behavior and I certainly couldn't find my way to labeling it as incorrect.

from typescript.

erik-kallen avatar erik-kallen commented on April 28, 2024

Thank you, that was a good explanation.

you can't soundly assume that what you have isn't a malformed T1

This is probably for the better, then. It's probably better to reject some valid programs than to accept more invalid ones.

from typescript.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.