The Wayback Machine - https://web.archive.org/web/20210102142400/https://github.com/microsoft/TypeScript/pull/29317
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

Negated types #29317

Open
wants to merge 2 commits into
base: master
from
Open

Negated types #29317

wants to merge 2 commits into from

Conversation

@weswigham
Copy link
Member

@weswigham weswigham commented Jan 9, 2019

Long have we spoken of them in hushed tones and referenced them in related issues, here they are:

Negated Types

Negated types, as the name may imply, are the negation of another type. Conceptually, this means that if string covers all values which are strings at runtime, a "not string" covers all values which are... not. We had hoped that conditional types would by and large subsume any use negated types would have... and they mostly do, except in many cases we need to apply the constraint implied by the conditional's check to it's result. In the true branch, we can just intersect the extends clause type, however in the false branch we've thus far been discarding the information. This means unions may not be filtered as they should (especially when conditionals nest) and information can be lost. So that ended up being the primary driver for this primitive - it's taking what a conditional type false branch implies, and allowing it to stand alone as a type.

Syntax

not T

where T is another type. I'm open to bikeshedding this, or even shipping without syntax available, but among alternatives (!, ~) not reads pretty well.

Identities

These are little tricks we do on negated type construction to help speed things along (and give negations on algebraic types canonical forms).

  • not not T is T
  • not (A | B | C | ...) is not A & not B & not C & not ...
  • not (A & B & C & ...) is not A | not B | not C | not ...
  • not unknown is never
  • not never is unknown
  • not any is any (since any is the NaN of types and behaves as both the bottom and top)

Assignability Rules

Negated types, for perhaps obvious reasons, cannot be related structurally - the only sane way to relate them is in a higher-order fashion. Thus, the rules governing these relations are very important.

  • A negated type not S is related to a negated type not T if T is related to S.
    This follows from the set membership inversion that a negation implies - if normally a type S and a type T would be related if S is a subset of T, when we take the complements of those sets, not S and not T, those sets share an inverse relationship to the originals.
  • A type S is related to a negated type not T if the intersection of S and T is empty
    We want to check if for all values in S, none of those values are also in T (since if they are, S is not in the negation of T). The intersection of S and T, when simplified and evaluated, is exactly the description of the common domain of the two. If this domain is empty (never), then we can conclude that there is no overlap between the two and that S must lie within not T.
  • A negated type not S is not related to a type T.
    A negated type describes a set of values that reaches from unknown to its bound, while a normal type describes values from its bound to never - it's impossible for a negated type to satisfy a normal type

Assignability Addendum for Fresh Object Types

Frequently we want to consider a fresh object type as a singleton type (indeed, some examples in the refs assume this) - it corresponds to one runtime value, not the bounds on a value (meaning, as a type, both its upper and lower bounds are itself). Using this, we can add one more rule that allows fresh literal types to easily satisfy negated object types.

  • A fresh object type S is related to a negated type not T if S is not related to T.
    Since S is a singleton type, we can assume that so long as it's type is not in T, then it is in not T.

Examples

Examples of negated type usage can be found in the tests of this PR (there's a few hundred lines of them, and probably some more to come for good measure), but here's some of the common ones, pulled from the referenced issues:

declare function ignore<T extends not (object & Promise<any>)>(value: T): void;
declare function readFileAsync(): Promise<string>;
declare function readFileSync(): string;
ignore(readFileSync());     // OK
ignore(readFileAsync());    // Should error

declare function map<T, U extends not void>(values: T[], map: (value: T) => U) : U[]; // validate map callback doesn't return void

function foo() {}

map([1, 2, 3], n => n + 1); // OK
map([1, 2, 3], foo);        // Should error

function asValid<T extends not null>(value: T, isValid: (value: T) => boolean) : T | null {
    return isValid(value) ? value : null;
}

declare const x: number;
declare const y: number | null;
asValid(x, n => n >= 0);    // OK
asValid(y, n => n >= 0);    // Should error

function tryAt<T extends not undefined>(values: T[], index: number): T | undefined {
    return values[index];
}

declare const a: number[];
declare const b: (number | undefined)[];
tryAt(a, 0);    // OK
tryAt(b, 0);    // Should error

Fixes #26240.
Allows #27711 to be cleanly fixed with a lib change (example in the tests).

Ref #4183, #4196, #7648, #12215, #18280
@@ -46,7 +46,6 @@ tests/cases/conformance/types/keyof/keyofAndIndexedAccessErrors.ts(87,5): error
tests/cases/conformance/types/keyof/keyofAndIndexedAccessErrors.ts(103,9): error TS2322: Type 'Extract<keyof T, string>' is not assignable to type 'K'.
Type 'string & keyof T' is not assignable to type 'K'.
Type 'string' is not assignable to type 'K'.
Type 'string' is not assignable to type 'K'.

This comment has been minimized.

@DanielRosenwasser

DanielRosenwasser Jan 9, 2019
Member

I'm not complaining, but why'd this go away?

This comment has been minimized.

@weswigham

weswigham Jan 9, 2019
Author Member

I accidentally fixed a bug where we duplicated the elaboration - this is as the same as the line above.

@DanielRosenwasser
Copy link
Member

@DanielRosenwasser DanielRosenwasser commented Jan 9, 2019

First take, without having tried this out:

  • Intersections appear to be the correct mechanism to check for potential overlap, but that also means almost no object type can satisfy a negated object type. If we add negated types, I would prefer not to give the check up at all though.
  • I find it weird, but understandable, that you treat fresh object types differently. But it's clearly weird that these are the only exception to the above issue.

I do have some reservations on the feature for these reasons. If you think about our features trying to satisfy convenience, intent, and safety, then I don't know if this appropriately weighs convenience and intent.

@weswigham
Copy link
Member Author

@weswigham weswigham commented Jan 9, 2019

Intersections appear to be the correct mechanism to check for potential overlap, but that also means almost no object type can satisfy a negated object type. If we add negated types, I would prefer not to give the check up at all though.

not (object & SomeInterface)

It works just dandy~

@DanielRosenwasser
Copy link
Member

@DanielRosenwasser DanielRosenwasser commented Jan 9, 2019

It works just dandy~

What do you mean? Yesterday you mentioned that you couldn't assign an array to a not PromiseLike<any>.

@weswigham
Copy link
Member Author

@weswigham weswigham commented Jan 9, 2019

Nope, you can't, because you can trivially make a

interface PromiseLikeArray extends Array<any> implements PromiseLike<any> { /*...*/ }

so, if you go to the example, I just state that I explicitly return an array that isn't PromiseLike - that is T[] & not PromiseLike<any>

@weswigham
Copy link
Member Author

@weswigham weswigham commented Jan 9, 2019

Per offline feedback from @ahejlsberg I've changed from ~ unary operator to a not keyword type operator (and updated all the text in this PR thus far to match). It does read nicer.

@weswigham weswigham closed this Jan 9, 2019
@weswigham weswigham reopened this Jan 9, 2019
@Jessidhia
Copy link

@Jessidhia Jessidhia commented Jan 10, 2019

Can this be used as a way to restrict the potential type of unbounded types? For example, number & not 0 to allow any number except the literal 0.

This doesn't look very useful at first glance but it can be powerful on mapped types. For example, [key in string and not keyof CSS.Properties<any>] in https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e836acc75a78cf0655b5dfdbe81d69fdd4d8a252/types/styled-components/index.d.ts#L16-L25 could allow the constrained index signature to not have to include CSS.Properties<string | number>[keyof CSS.Properties<string | number>].

@weswigham
Copy link
Member Author

@weswigham weswigham commented Jan 10, 2019

Can this be used as a way to restrict the potential type of unbounded types? For example, number & not 0 to allow any number except the literal 0.

Yep. That's a primary driver for 'em.

@Jessidhia
Copy link

@Jessidhia Jessidhia commented Jan 10, 2019

type Exact<T extends object> = T & Partial<Record<not keyof T, never>>

🤔

@jack-williams
Copy link
Collaborator

@jack-williams jack-williams commented Jan 10, 2019

Is there an outline of how not interacts with narrowing? Might we have something like?

function foo(x: unknown) {
  if (typeof x === "number") {
    const num: number = x;
  } else {
    const numNot: not number = x;
  }
}

Assignability Addendum for Fresh Object Types

Is there a short example that demonstrates wanting a not for an object literal?


@DanielRosenwasser Do you mean something like { x: number } is not assignable to not { x: boolean } because { x : number} & { x: boolean } does not get reduced to never.

Meta question for @DanielRosenwasser: You say:

convenience, intent, and safety

that seem like some internal principles the TS team have for designing features? Is there a public description of these? I think it would help feature proposals if external contributors could frame their suggestions with the same language used internally.


@Kovensky I'm not sure that will type-check. The type not keyof T includes any type which isn't one of the keys, such as boolean. You might need:

type Exact<T extends object> = T & Partial<Record<(not keyof T) & string, never>>

though I'm not sure what the semantics will be for not types appearing in mapped type constraints.

@weswigham
Copy link
Member Author

@weswigham weswigham commented Jan 10, 2019

Is there an outline of how not interacts with narrowing? Might we have something like?

In this PR no negated types are produced by control flow yet; however we've talked over it and negated types make the lib type facts PR elegant to implement, since we can skip using conditionals (which don't compose well) and just filter with intersections of negated types :D

Is there a short example that demonstrates wanting a not for an object literal?

Aye, a test case with an example I pulled from a related issue:

// from https://github.com/Microsoft/TypeScript/issues/4183
type Distinct<A, B> = (A | B) & not (A & B);
declare var o1: {x};
declare var o2: {y};

 declare function f1(x: Distinct<typeof o1, typeof o2>): void;

 f1({x: 0});         // OK
f1({y: 0});         // OK
f1({x: 0, y: 0});   // Should error

though I'm not sure what the semantics will be for not types appearing in mapped type constraints.

Right now they're quietly dropped (aside from filtering out mismatching concrete types), like symbol for the same reason. There's no non-generic concept to map them into. I have a test to that effect. The arbitrary index signatures PR fixes this and would allow an index signature of a, say, string & not "". With that in place mapped types work correctly with 'em, since the intersections trivially desugar to an index signature.

@jack-williams
Copy link
Collaborator

@jack-williams jack-williams commented Jan 10, 2019

@weswigham Thanks! And this PR is very cool :)

Re: the object literal example. Should that not be a case where EPC raises an error? I know it doesn't right now because there is no discriminant, but it probably should. Will using negation types become the canonical way of dealing with examples like this? If not, and assuming EPC does get fixed, are there many other use-cases for the special object literal relation.

@weswigham
Copy link
Member Author

@weswigham weswigham commented Jan 10, 2019

Re: the object literal example. Should that not be a case where EPC raises an error? I know it doesn't right now because there is no discriminant, but it probably should. Will using negation types become the canonical way of dealing with examples like this? If not, and assuming EPC does get fixed, are there many other use-cases for the special object literal relation.

Even if excess property checking makes defining a type like Distinct unneeded, you'd still need the relationship to allow a type like Distinct to be satisfiable (with fresh object types) should one be used.

@SalathielGenese
Copy link

@SalathielGenese SalathielGenese commented Jan 15, 2019

Good job so far, I delayed some projects to wait for this feature - for more than a year.

@weswigham weswigham mentioned this pull request Jan 16, 2019
@jack-williams
Copy link
Collaborator

@jack-williams jack-williams commented Jan 16, 2019

Do these identities hold?
T & (not T) is never.
T & (not U) is T when T and U are disjoint.
"2b" | (not "2b") is unknown, or generally T | (not T) is unknown.

Just wondering what it would take to fix #28131

@weswigham
Copy link
Member Author

@weswigham weswigham commented Jan 16, 2019

T & (not T) is never.

Yes. More generally, when U extends T, U & not T is never.

T & (not U) is T when T and U are disjoint.

Yes.

"2b" | (not "2b") is unknown, or generally T | (not T) is unknown

More generally when U extends T, T | not U is just unknown (quite literally the opposite of the intersection into never rule). That's correct though I don't remember if I'd actually implemented this one yet - afaik we don't currently have any union identities that cause unions to "simplify" to unknown yet so I remember thinking about how it's best accomplished for a bit.

const result = isRelatedTo(source, (target as NegatedType).type);
return result === Ternary.Maybe ? Ternary.Maybe : result ? Ternary.False : Ternary.True;
}
// Relationship check is S ⊂ T

This comment has been minimized.

@DanielRosenwasser

DanielRosenwasser Jan 23, 2019
Member

Do you really need to use this symbol? It almost sounds like a joke, but this could affect memory footprint when bootstrapping the compiler since modern engines can avoid full UTF16 representations https://blog.mozilla.org/javascript/2014/07/21/slimmer-and-faster-javascript-strings-in-firefox/

This comment has been minimized.

@peter-leonov

peter-leonov Jan 23, 2019

A minification step will simply remove this line. The article suggests to minify the code to utilise the inline strings and also help with strings interning. I guess, v8 should not be so much different in this sense and also win from code minification.

@RyanCavanaugh RyanCavanaugh added this to This Week in Design Meeting Docket Jan 23, 2019
@jack-williams
Copy link
Collaborator

@jack-williams jack-williams commented Jan 24, 2019

@weswigham Seems to work, right?

type UnionToInterWorker<T> = not (T extends T ? not T : never);
type UnionToInter<T> = [T] extends [never] ? never : UnionToInterWorker<T>;

Not quite sure what should happen here:

type NotInfer<T> = T extends not (infer U) ? U : never;
type NotString = NotInfer<string> // string extends not U ? U : never;

Currently it doesn't reduce; maybe infer types should be disallowed under not?

@weswigham
Copy link
Member Author

@weswigham weswigham commented Jan 24, 2019

It doesn't reduce because we don't reduce reducible conditionals with infer's in them right now. IMO, it certainly still makes sense to allow an infer inside a not, since not A extends not infer B ofc should infer A for B.

@jack-williams
Copy link
Collaborator

@jack-williams jack-williams commented Jan 24, 2019

Yep, you're right. I guess it could infer U = not string?

@jamiebuilds
Copy link

@jamiebuilds jamiebuilds commented Jul 14, 2020

@weswigham Is there a way we can test this out on the playground/in our projects, I saw there was a special build for #38305. Could we have the same here to give feedback?

@orta
Copy link
Member

@orta orta commented Jul 15, 2020

It may need a rebase for that, but we can see
@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

@typescript-bot typescript-bot commented Jul 15, 2020

Heya @orta, I've started to run the tarball bundle task on this PR at 2ef105c. You can monitor the build here.

@dgreene1
Copy link

@dgreene1 dgreene1 commented Sep 14, 2020

Would this enable us to inform consumers of functions that they can't pass an array?

Please excuse the contrived example. But it illustrates the signature I'm looking to write.

function getObjectKeys<T extends object not Array<unknown>>(input: T) {
  return Object.keys(input);
}

Because the current need to have to throw at runtime feels like it's against TypeScript's goals of catching bugs at runtime. i.e.

function getObjectKeys<T extends object not Array<unknown>>(input: T) {
  if(Array.isArray(input)){
    throw new Error('expected an object, not an array')
  }
  return Object.keys(input);
}

If this change does not plan to solve this use case, please let me know and I can make a new issue.

@isiahmeadows
Copy link

@isiahmeadows isiahmeadows commented Sep 15, 2020

@dgreene1

function getObjectKeys<T extends object & not Array<unknown>>(input: T) {
  return Object.keys(input);
}

You forgot an &, but that's how you'd do it.

@thw0rted
Copy link

@thw0rted thw0rted commented Sep 15, 2020

I guess the not construct is maybe clearer but there are options today, like function foo<T extends {[k: string]: any, [n: number]: never}>(arg: T). Of course not every object with number keys is an Array, but it will flag any Array passed at runtime unless it's an empty literal (or you explicitly type it as Array<never>).

@ExE-Boss
Copy link
Contributor

@ExE-Boss ExE-Boss commented Sep 15, 2020

@dgreene1
You might want to do not ReadonlyArray<unknown>, since that covers both read‑only arrays and read‑write arrays.

@dgreene1
Copy link

@dgreene1 dgreene1 commented Sep 15, 2020

I guess the not construct is maybe clearer but there are options today, like function foo<T extends {[k: string]: any, [n: number]: never}>(arg: T). Of course not every object with number keys is an Array, but it will flag any Array passed at runtime unless it's an empty literal (or you explicitly type it as Array).

I appreciate the suggestion, but that solution does not work for all cases. In fact, it might be a little dangerous because it requires the input type to have an iterable index which might lead developers to add an index signature to their interfaces once they see this error:

Argument of type 'ITestCase' is not assignable to parameter of type 'ObjectNotArray'. Index signature is missing in type 'ITestCase'.ts(2345)

So ultimately I think I'll stick to the runtime check until this PR is approved. Thank you @weswigham for creating this PR and thank you @isiahmeadows / @ExE-Boss for confirming the not solution. @DanielRosenwasser, I believe that my scenario above clarifies the value of this PR. Have I mentioned how much I love the Typescript community? :) Cause I do. ❤️

@mohe2015
Copy link

@mohe2015 mohe2015 commented Sep 19, 2020

I would really love to try this out with TypeScript 4.1 but the conflicts are massive and unmanagable to resolve if you don't know a lot about the internals. Would you be willing to rebase @weswigham? I know this is a lot to ask for.

@AmitDigga
Copy link

@AmitDigga AmitDigga commented Oct 4, 2020

A thought on identity rules.

I think these should be the proper identities

1 not not T is T
2 not (A | B | C | ...) is not A & not B & not C & not ...
3 not (A & B & C & ...) is not A | not B | not C | not ...
4 not any is never
5 not never is any
6 not unknown is either unknown or illegal keyword combination.

Explanation

  1. not any is never

    • we know that any is union of all types or any is (string | bool | number | object | function | null | undefined)

    • Then not any is not string & not bool & not number & not object & not function & not null & not undefined [ From identity that not (A | B | C | ...) is not A & not B & not C & not ...]

    • It means that not any cannot be anything. So its better to say not any is never

  2. not never is any

    • not never == not never
    • not never == not (not any) [ From updated identity of not any is never]
    • not never == any [ From identity that not not T is T]
  3. Exploring case of not unknown

    1. not unknown is unknown

      • If we do not know the type of x, then how can we know the type of not x
      • Suppose we have a set TypeSet == { A, B, C }. Now say x belongs to one of the type from TypeSet, but is not clearly know. So we can say that x is unknown.
        If x is A, then not x is { B, C }
        If x is B, then not x is { A, C }
        If x is C, then not x is { A, B }
        So by this we can say that if x is unknown then not x is also unknown.
    2. not unknown is illegal keyword combination.
      By linguistic design not unknown should be known. But it does mean anything, as there is no type meaning known. If I assign id : not unknown, it doesn't mean that its automatically going to derive to string or number depending upon my use case. So not unknown can be invalid syntax in type system.

@isiahmeadows
Copy link

@isiahmeadows isiahmeadows commented Oct 5, 2020

@AmitDigga I would've thought that since unknown represented the top type (all values are assignable to it), not unknown would evaluate to never (as no values are assignable to it).

@dimitropoulos
Copy link

@dimitropoulos dimitropoulos commented Oct 5, 2020

this will not become a problem, right?

  1. not any == never
  2. not unknown == never
  3. not any == not unknown
  4. any == unknown
@Jack-Works
Copy link
Contributor

@Jack-Works Jack-Works commented Oct 5, 2020

@dimitropoulos

1*0=0 and 2*0=0 doesn't make 1=2, maybe this relation also applies to the not operator

@dimitropoulos
Copy link

@dimitropoulos dimitropoulos commented Oct 5, 2020

right, totally get that. that's why I was asking (to just make sure) that we think we will not run into an issue with identity related to how not any and not unknown evaluate.

@AmitDigga
Copy link

@AmitDigga AmitDigga commented Oct 5, 2020

Its little confusing to me, but let me explain my point

We are dealing with set theory. So, lets say if our universal set U is {bool, string, number, object, function, undefined, null, void}

  1. By our definition of any we know that any = U = {bool, string, number, object, function, undefined, null, void}
  2. never is empty set = {}
  3. unknown is some subset from U say Unknown.

Expansion

  1. not function is
    = U - function
    = {bool, string, number, object, undefined, null, void}

  2. not any is
    = U - any
    = U - U
    = {}
    = never

  3. not never is
    = U - {}
    = U
    = any

  4. not Unknown is
    = U - Unknown
    = a subset from U, which is still not known
    = unknown

@isiahmeadows Can you clear confusion over here. I sill think that any is universal set, and unknown is some subset of universal set, which is not known yet.

@dimitropoulos
Either not any == never is going to be true or not unknown == never is going to true. Both cannot be true since any is not equal to unknown

@Jack-Works dividing by 0 analogy wont work over here as dividing by zero gives undefined. In set theory, not of something is clearly defined.
If not X == not Y, it only means that X == Y.

@mheiber
Copy link
Contributor

@mheiber mheiber commented Oct 5, 2020

Re @AmitDigga #29317 (comment)

Its little confusing to me, but let me explain my point

We are dealing with set theory. So, lets say if our universal set U is {bool, string, number, object, function, undefined, null, void}

  1. By our definition of any we know that any = U = {bool, string, number, object, function, undefined, null, void}
  2. never is empty set = {}
  3. unknown is some subset from U say Unknown.

...
I sill think that any is universal set, and unknown is some subset of universal set, which is not known yet.

If you're thinking of types as sets where "T ⊆ U" encodes "T is assignable to U" then:

  • never is the empty set
  • unknown is the universe
  • any is a lawless monster (behaves as both never and unknown depending on its mood)

In terms of assignability:

  • never:
    • nothing is assignable to never except never (the empty set has no proper subset)
    • something of type never is assignable to anything (the empty set is a subset of every set)
  • unknown:
    - anything is assignable to unknown. (all sets are subsets of the universe)
    - unknown is not assignable to anything except unknown (the universe is not a subset of anything except universe)

I'm using "universe" here to refer to the set which has all other sets as subsets (top)–which exists in our case.

TS types aren't really sets, it's just a metaphor that works most of the time.

Re "unknown is some subset of universal set, which is not known yet":

unknown, name aside, has no more to do with knowledge than any other type. It's probably called "unknown" just because it's the most general: the more general a type is, the less informative it is that something inhabits the type. For example, if something is a string | number we know a lot less about it than if we know it is a string. And if something is unknown then we know nothing about what it is. We can safely assign anything to a variable of type unknown: that's fine, we just forget what we know. But we can't safely treat an unknown as a string because it might not be a string.

@isiahmeadows
Copy link

@isiahmeadows isiahmeadows commented Oct 5, 2020

@AmitDigga

@isiahmeadows Can you clear confusion over here. I sill think that any is universal set, and unknown is some subset of universal set, which is not known yet.

Here's how those two narrows in practice, if it helps: Playground Link

@lgenzelis
Copy link

@lgenzelis lgenzelis commented Nov 3, 2020

It would amazing to have this functionality! Should we expect this PR to be included in some minor release of Typescript 4? Or is that too far fetched?

@weswigham
Copy link
Member Author

@weswigham weswigham commented Nov 3, 2020

Hmm, so the root feature, as described in this PR isn't too contentious (there's some hesitation because the concept may be too complex), but the driver for implementing the feature, using them in control flow to more precisely narrow negative branches on generics, last time I checked (in a different branch based on this PR) had bad performance implications that I couldn't easily work around. Other changes, like caching relationship results on intersection comparisons (we have an open pr for that), or better alias finding (and that) might improve that performance profile.

@thw0rted
Copy link

@thw0rted thw0rted commented Nov 3, 2020

I think I've been following this PR in hopes that negated-types gives me an easy way to express index signatures with exceptions, e.g.

class Foo {
  bar(): boolean;
  baz(): number;
  [k: string & not ("bar" | "baz")]: string;
}

const f = new Foo();
foo.bar(); // boolean
foo.catz; // string

If I'm remembering how I got here correctly, just as a data point, there are a lot more people like me waiting for a simple solution to "indexable classes" (without using intersection types).

@Jack-Works
Copy link
Contributor

@Jack-Works Jack-Works commented Nov 14, 2020

@thw0rted you can do it today using a hack:

class Foo {
    // @ts-ignore
    bar(): void {}
    // @ts-ignore
    baz(): boolean { return true }
    [k: string]: string;
}

const f = new Foo();
f.bar(); // void
f.catz; // string
f.baz(); // boolean
@thw0rted
Copy link

@thw0rted thw0rted commented Dec 7, 2020

@Jack-Works that's a more palatable way of keeping a broken declaration inside one class, but it's still broken for interfaces:

interface Oops {
  [k: string]: string; 
  // @ts-ignore
  bar: number;
  // @ts-ignore
  baz: boolean;
}

// "Property 'bar' is incompatible with index signature."
const o: Oops = {
  bar: 1,
  baz: true,
  catz: "hey"
};

I haven't had a chance to try this branch yet but I think it would solve both cases, without any messy ts-ignores.

@Jack-Works
Copy link
Contributor

@Jack-Works Jack-Works commented Dec 7, 2020

@thw0rted please checkout #41524

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.