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

Suggestion: treat in operator as type guard which asserts property existence #21732

Closed
jcalz opened this issue Feb 7, 2018 · 98 comments · Fixed by #50666
Closed

Suggestion: treat in operator as type guard which asserts property existence #21732

jcalz opened this issue Feb 7, 2018 · 98 comments · Fixed by #50666
Labels
Committed The team has roadmapped this issue Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Fix Available A PR has been opened for this issue Help Wanted You can do this Suggestion An idea for TypeScript
Milestone

Comments

@jcalz
Copy link
Contributor

jcalz commented Feb 7, 2018

TypeScript Version: 2.8.0-dev.20180204

Search Terms: in operator type guard generic assert

Code

function f<K extends string, T extends object>(key: K, genericObj: T, concreteObj: {foo: string}) {
  if ('a' in concreteObj) {
    concreteObj.a // error, Property 'a' does not exist on type 'never'.
  }
  if ('a' in genericObj) {
    genericObj.a // error, Property 'a' does not exist on type 'T'.
  }
  if (key in concreteObj) {
    concreteObj[key]; // error, Type 'K' cannot be used to index type '{ foo: string; }'
  }
  if (key in genericObj) {
    genericObj[key] // error, Type 'K' cannot be used to index type 'T'.
  }
}

Actual behavior:
The compiler does not recognize that the objects have relevant keys even after checking for the existence of the key with the in operator. According to a comment by @sandersn, the in type guard (as implemented in #15256) narrows by eliminating members from a union; it does not assert that a key exists.

Desired behavior:
The compiler would assert that each object had a property with the relevant key, after having checked for the existence of the key with the in operator. Note that one possible implementation of an asserting type guard would look like

function inOperator<K extends string, T extends object>(k: K, o: T): o is T & Record<K, unknown> {
  return k in o;
}

but this does not behave exactly as desired, possibly due to the bug in #18538: (not sure if #18538 was officially fixed, but it's not erroring anymore)

function g<K extends string, T extends object>(key: K, genericObj: T, concreteObj: { foo: string }) {
  if (inOperator('a', concreteObj)) {
    concreteObj.a // okay
  }
  if (inOperator('a', genericObj)) {
    genericObj.a // okay
  }
  if (inOperator(key, concreteObj)) {
    concreteObj[key]; // okay
  }
  if (inOperator(key, genericObj)) {
    genericObj[key] // okay
  }
}

If a fix for #18538 appears and makes the g() function compile without error, great. Otherwise, maybe the property assertion for in could happen some other way. Not sure.

Playground Link: Here

Related Issues:
#10485, Treat in operator as type guard
#15256, Add type guard for in keyword
#18538, Error when mixing keyof and intersection type and type variable (fixed?)

(EDIT: #18538 seems to be fixed)
(EDIT: change any to unknown)

@mhegazy
Copy link
Contributor

mhegazy commented Feb 7, 2018

related discussion also in #10715 about type guards "evolving" the type of an expression.

@mhegazy mhegazy added the Suggestion An idea for TypeScript label Feb 7, 2018
@GregRos
Copy link

GregRos commented Feb 12, 2018

This! Currently, we have the odd situation that when you write:

if ("assign" in Object) {
    Object.assign(target, ...args);
}

When the ES6 typings aren't loaded, it will narrow Object down to never in the if statement!

@mhegazy
Copy link
Contributor

mhegazy commented Feb 12, 2018

@GregRos please see #21517

@GregRos
Copy link

GregRos commented Feb 14, 2018

@mhegazy Yup, I know. I was giving it as an example for what this suggestion would fix. I know why it's happening.

@mattmccutchen
Copy link
Contributor

mattmccutchen commented Jul 17, 2018

Let's add some other ways of asserting property existence from #25720 to this proposal:

let x: unknown;

// All these should narrow x to {prop: unknown} (s/any/unknown/ from original proposal by Matt)
"prop" in x;
x.prop != null;
x.prop !== undefined;
typeof x.prop !== "undefined";

// typeof should work on properties of the unknown variable
typeof x.prop === "string"; // should narrow x to {prop: string}

@mhegazy
Copy link
Contributor

mhegazy commented Jul 27, 2018

Similar requests in #10715, #25720, and #25172

@leighman
Copy link

leighman commented Aug 9, 2018

@mattmccutchen I think you'd want those to narrow to {prop: unknown} rather than {prop: any} wouldn't you?

@felipeochoa
Copy link

@mattmccutchen The problem is that trying to access a property on null/undefined will throw an error at runtime. I would support adding those type guards, but only on the object type. (If there were a way to get an unknownWithoutNullOrUndefined type, that would be even better)

@RyanCavanaugh
Copy link
Member

@jcalz can you highlight the differences between #10485 and what you'd want to happen?

@jcalz
Copy link
Contributor Author

jcalz commented Aug 14, 2018

TL;DR: #10485 narrows by filtering union constituents. This suggestion narrows by adding properties to the type.

In cases where y is not a union type or none of the constituents have an explicit property named x, the test if (x in y) should not try to filter union constituents, but should instead narrow y by intersecting its type with Record<typeof x, unknown>.


In #10485, the type of the object you're checking with in is meant to be a union, and the type guard filters that union based on whether the constituents do or do not explicitly have a property of the relevant name:

type A = { w: string, x: number };
type B = { y: string, z: number };
function foo(p: A | B): number {
  if ('w' in p) {
    return p.x; // p is narrowed to A
  } else {
    return p.z; // p is narrowed to B
  }
}

Note that this isn't exactly sound, since you can call

foo({ y: "oops", z: 100, w: true }); 

but the point of property checking on unions is usually to use that property as a discriminant, and the sound behavior would probably annoy the heck out of developers.


Compare to the following:

function bar(p: {}): string {
  if ('w' in p) {
    return String(p.w); // error?! ☹
  } else {
    return "nope";
  }
}

It is surprising that after what feels like an explicit test for p.w, the compiler still doesn't know that p.w exists. Worse, p is narrowed to never, probably because of #10485.

What I want to see happen is:

@RyanCavanaugh RyanCavanaugh added the In Discussion Not yet reached consensus label Aug 14, 2018
@sirian
Copy link
Contributor

sirian commented Aug 19, 2018

@jcalz This guard doesn't work with partial interfaces

function foo(x: {foo: "bar"} | {toFixed?: () => any}) {
  if (inOperator("toFixed", x)) {
    x.toFixed(); // Error, Object is of type unknown
  }
}

@jcalz
Copy link
Contributor Author

jcalz commented Aug 19, 2018

@sirian Yeah, strictly speaking, all you know is that x.toFixed exists, since a value of type {foo: "bar"} may contain a toFixed property (e.g.,

foo(Object.assign({ foo: "bar" as "bar" }, { toFixed: "whoops" }));  // no error

), but assuming people still want the current behavior in #10485 where we eliminate {foo: "bar"} from consideration as soon as we find a toFixed property, then this suggestion is to apply the inOperator() behavior after that elimination.

So in your case, x would first be narrowed to {toFixed?: () => any} as per #10485 and then to {toFixed: (() => any) | undefined} via something like inOperator()... meaning that toFixed is definitely there but it might be undefined (since ? is ambiguous about whether the property is actually missing or present but undefined.)

If you want a better idea what the behavior would end up being like, try the following complex user-defined type guard using conditional types:

type Discriminate<U, K> = ( U extends any ? K extends keyof Required<U> ? U : never : never ) 
  extends infer D ? [D] extends [never] ? U : D : never
        
function hybridInOperator<K extends keyof any, T>(
  k: K, 
  o: T
): o is Discriminate<T, K> & Record<K, unknown> {
    return k in o;
}

producing:

function foo(x: { foo: "bar" } | { toFixed?: () => any }) {
    if (hybridInOperator("toFixed", x)) {
      x.toFixed(); // error, possibly undefined
      if (x.toFixed) x.toFixed();   // okay
    }
}

Does that seem better?

@sirian
Copy link
Contributor

sirian commented Aug 20, 2018

@jcalz
Better, but there is another problem with type infer. Look at example

function foo(x: { foo: "bar" } | Number | { toFixed?: () => any }) {
    if (hybridInOperator("toFixed", x)) {
        x.toFixed(); // no error. since x resolved as Number
        if (x.toFixed) x.toFixed();   // okay
    }
}

I also tried various type guards. But always found a new counterexample(

Upd. As a quick fix - if you change o is Discriminate<T, K> & Record<K, unknown> to o is Extract<T, Discriminate<T, K>>. then x will be resolved as Number | { toFixed?: () => any }

Upd2.

So in your case, x would first be narrowed to {toFixed?: () => any} as per #10485 and then to {toFixed: (() => any) | undefined}

Not right... toFixed would be narrowed to unknown... Look at example. So and error in #21732 (comment) screenshot was not "object is possibly undefined"

image

@jcalz
Copy link
Contributor Author

jcalz commented Aug 20, 2018

I don't know how worthwhile it is to come up with a user-defined type guard which exactly mimics the desired behavior of in narrowing, in light of the variety of edge cases. In this case, something weird is happening, since the hybridOperator() returns something like x is Number | { toFixed: undefined | (() => any) }; which is wider than Number. The fact that x narrows to Number inside the then-clause looks like some kind of compiler bug (edit: as per #26551, it is not considered a bug, but it is inconsistent and makes it more difficult to design a perfect type guard). I'd rather not get sidetracked here with that.

The "quick fix" has toFixed as optional, but it should be required... which is why I was intersecting with {toFixed: unknown} in the first place.

re: Upd2, I think you're missing that the suggestion is to apply the inOperator() behavior after the elimination that happens in #10485 ? Not sure if I wasn't clear about that.

@sirian
Copy link
Contributor

sirian commented Aug 20, 2018

@jcalz Maybe add smth like & Pick<Required<T>, K & keyof <Required<T>>>? I'll try to experiment at home )

@sirian
Copy link
Contributor

sirian commented Aug 20, 2018

@jcalz what about this one?

type Discriminate<U, K extends PropertyKey> =
    U extends any
    ? K extends keyof U ? U : U & Record<K, unknown>
    : never;

function inOperator<K extends PropertyKey, T>(k: K, o: T): o is Discriminate<T, K> {
    return k in o;
}

function foo(x: null | string | number | Number | { toFixed?: () => any } | { foo: 1 }) {
    if (inOperator("toFixed", x)) {
        // x is number | Number | { toFixed?: () => any | undefined }
        x.toFixed && x.toFixed();
    }
}

function g<K extends string, T>(key: K, genericObj: T, concreteObj: {foo: string}) {
    if (inOperator('a', concreteObj)) {
        concreteObj.a // unknown
    }
    if (inOperator('a', genericObj)) {
        genericObj.a // any
    }
    if (inOperator(key, concreteObj)) {
        concreteObj[key]; // unknown
    }
    if (inOperator(key, genericObj)) {
        genericObj[key] // any
    }
}

@jcalz
Copy link
Contributor Author

jcalz commented Aug 20, 2018

Looks good at first glance!

@sirian
Copy link
Contributor

sirian commented Aug 21, 2018

@jcalz There is a dilemma. how should work typeguard?

function foo(x: {foo?: number} | {bar?: string}) {
    if (inOperator("foo", x)) {
        // x is {foo: number | undefined}
        // or
        // x is {foo: number | undefined} | {foo: unknown, bar?: string} 
    }
}

or in this case:

function foo(x: {foo?: number} | string) {
    if (inOperator("foo", x)) {
        // x is {foo: number | undefined}
        // or
        // x is {foo: number | undefined} |  string & {foo: unknown} 
    }
}

@jcalz
Copy link
Contributor Author

jcalz commented Aug 21, 2018

It should be the first one in both cases, {foo: number | undefined}, as implied by the bulleted list at the bottom of this comment.

@sirian
Copy link
Contributor

sirian commented Aug 21, 2018

@jcalz So if we extract types rather than widen it with additional property - what should be at this examples?

function foo(x: {bar?: number}) {
    if (inOperator("foo", x)) {
        // x is never? so is x.foo not accessible?
    }
}
function foo(x: object) {
    if (inOperator("foo", x)) {
        // x is never? so is x.foo not accessible?
    }
}

@jcalz
Copy link
Contributor Author

jcalz commented Aug 21, 2018

It should be {bar?: number, foo: unknown} and {foo: unknown} respectively, as implied by the same bulleted list. I can't tell if that isn't clear or if you're not reading it. 🤕

@bennycode
Copy link
Contributor

I recently came across this problem:

const person = {
  "name": "Benny"
} as unknown;

if (person && typeof person === "object" && "name" in person) {
  console.log(person.name);
}

I am narrowing person from undefined to an object and afterwards I am checking if name is in person but the "in" type guard doesn't seem to catch that as I am getting the following error:

TS2339: Property 'name' does not exist on type 'object'.

Demo: https://www.typescriptlang.org/play?#code/MYewdgzgLgBADgUwE4XDAvDA3gKBjAIjAEMBbBAgLkICEEwwBPAnAXxmIhgFcwBrMCADuYANw4cASwBmMABSIUaAGTKYURohCzFqMBnSYCIAEYArBMCgEYqwiXI3J+3eACU2PDFCQQAGwQAOj8QAHMFZD1AhwQ3cVYgA

@iugo
Copy link

iugo commented Sep 2, 2022

I recently came across this problem:

const person = {
  "name": "Benny"
} as unknown;

if (person && typeof person === "object" && "name" in person) {
  console.log(person.name);
}

I am narrowing person from undefined to an object and afterwards I am checking if name is in person but the "in" type guard doesn't seem to catch that as I am getting the following error:

TS2339: Property 'name' does not exist on type 'object'.

Demo: https://www.typescriptlang.org/play?#code/MYewdgzgLgBADgUwE4XDAvDA3gKBjAIjAEMBbBAgLkICEEwwBPAnAXxmIhgFcwBrMCADuYANw4cASwBmMABSIUaAGTKYURohCzFqMBnSYCIAEYArBMCgEYqwiXI3J+3eACU2PDFCQQAGwQAOj8QAHMFZD1AhwQ3cVYgA

const person = {
  "name": "Benny"
} as unknown;

if (typeof person === "object" && person !== null) {
  const o = person as Record<string, unknown>
  if ('name' in o) {
    console.log(o.name);
  }
}

https://www.typescriptlang.org/play?#code/MYewdgzgLgBADgUwE4XDAvDA3gKBjAIjAEMBbBAgLkICEEwwBPAnAXxmIhgFcwBrMCADuYANw4cASwBmMABRRGiELMQo06TYRAAjAFYJgUAjABkp+MlRgYAQi1huAGycBKbHhihIsEBkvqNpwwAEqGIEgAJgA80EiSYADmADQ8-IIiAHyeMvIA5CTkeTAJMCDuuPj43qhOCAB0TiCJciD1hQiu4visbDhAA

@bennycode
Copy link
Contributor

Thanks for you demo, @iugo. Using the type assertion of as Record<string, unknown> feels a bit like cheating because it makes the typeof check unnecessary. I would like to narrow down an unknown type and by using an assertion I am just overriding the compiler's decision. It's like doing this:

const person = {
  "name": "Benny"
} as unknown as Record<string, unknown>;

console.log(person.name);

paulowe added a commit to paulowe/paulowe-web that referenced this issue Sep 4, 2022
@typescript-bot typescript-bot added the Fix Available A PR has been opened for this issue label Sep 7, 2022
@iugo
Copy link

iugo commented Sep 7, 2022

Because the data is unknown (such as data returned from a JSON string), we use unknown.

{
  const person = JSON.parse(`{"name": "Benny"}`) as unknown;
  check(person);
}

{
  const person = JSON.parse(`["Benny", 21]`) as unknown;
  check(person);
}

function check(person: unknown) {
  if (typeof person === "object" && person !== null) {
    const o = person as Record<string, unknown>
    if ('name' in o) {
      console.log(o.name);
    }

    if (Array.isArray(person)) {
      console.log(...person);
    }
  }
}

@bennycode
Copy link
Contributor

@iugo it looks like what I asked for will become possible soon! 🤩 #50666

@jcalz
Copy link
Contributor Author

jcalz commented Sep 19, 2022

It's happening!!!

@DetachHead
Copy link
Contributor

#50666 did not fully fix this issue:

I'm marking this PR as fixing #21732 even though it doesn't address the case of key in obj where key is of some generic type.

function f<K extends string, T extends object>(key: K, genericObj: T, concreteObj: {foo: string}) {
  if ('a' in concreteObj) {
    concreteObj.a // no longer an error
  }
  if ('a' in genericObj) {
    genericObj.a // no longer an error
  }
  if (key in concreteObj) {
    concreteObj[key]; // still an error
  }
  if (key in genericObj) {
    genericObj[key] // still an error
  }
}

playground

can this issue be re-opened? or is there a new issue for this?

@lazytype
Copy link

lazytype commented Feb 9, 2023

@DetachHead How would you expect this to work?

  // `concreteObj` is type `{foo: string}`
  if (key in concreteObj) {
    // `concreteObj` is narrowed to ???
    concreteObj[key];
  }

concreteObj being type {foo: string} doesn't mean that 'foo' is the only key in concreteObj. So let's say in the conditional we narrowed the type of concreteObj down to {foo: string} & Record<K, unknown>. Now there'd be no type error, but it would allow code like this:

function f<K extends string, T extends object>(key1: K, key2: K, genericObj: T, concreteObj: {foo: string}) {
  if (key1 in concreteObj) {
    concreteObj[key2];
  }

This is obviously unsafe, and there's no way in the type system to express that concreteObj has key1 but not key2.

@ehoogeveen-medweb
Copy link

Yeah, you'd need a construct like valueof key1 (so that in the 4th case, valueof key1 extends keyof genericObj). Not sure where excess properties would fit into that, though. Either way, noUncheckedIndexedAccess is very difficult to use without something like that (and without something to represent the inferred minimum length of arrays).

@bxt
Copy link

bxt commented Feb 15, 2023

I think it does not make sense that TypeScript assumes there are additional properties somehow on an object but also does not allow adding new properties to an object at the same time. Just like an if narrows down a type, adding properties to an object could narrow down the type to objects that also include this property. Consider this example:

const objectWithKnownKeys = {
    a: 1,
    b: 2,
} as const;

// This is an error but maybe it should work and just change
// the type of objectWithKnownKeys to include {c: 3} maybe?
objectWithKnownKeys.c = 3;

const key: string = 'a';

if (key in objectWithKnownKeys) {
    // TypeScript should know here there are no additional keys or allow setting c
    const value = objectWithKnownKeys[key];
}

So maybe the actual bug is that objectWithKnownKeys.c = generates an error? Basically TS2339 should not go off when setting. I can find an old issue #766 about this but related to classes. I would also allow to construct object like this instead of relying on any or Partial<>:

type SomeObject = { a: number, b?: number }
// We have to rely on "any", even Partial<SomeObject> would cause an error later
const initiallyEmptyObject: any = {};
initiallyEmptyObject.a = 1;
if (Math.random() > 0.5) { // or some logic
    initiallyEmptyObject.b = 2;
}
const someObject: SomeObject = initiallyEmptyObject;

But I assume this would be a pretty big change and there would need to be a solution for letting people know when they accidentally create a new property with a typo, so that's your reason why this will probably never be fixed.

@RyanCavanaugh
Copy link
Member

there would need to be a solution for letting people know when they accidentally create a new property with a typo

This is the entire crux of it; a computer can't tell whether this code is intentional or not:

const dimensions = { width: 0, height: 0 };
dimensions.widht = 3;

Our experience is that detecting retrospectively-obvious-to-human typos is seen as worth the false positives in other cases. It's why we have type annotations.

@aaroncowie
Copy link

there would need to be a solution for letting people know when they accidentally create a new property with a typo

This is the entire crux of it; a computer can't tell whether this code is intentional or not:

const dimensions = { width: 0, height: 0 };
dimensions.widht = 3;

Our experience is that detecting retrospectively-obvious-to-human typos is seen as worth the false positives in other cases. It's why we have type annotations.

Could "as const" be used to detect that case?

const dimensions = { width: 0, height: 0 } as const;
dimensions.widht = 3;

@DetachHead
Copy link
Contributor

if you want to allow adding unknown keys to an object, you can define it like so in the type:

interface Dimensions {
    width: number
    height: number
    [key: string]: number
}
const dimensions: Dimensions = { width: 0, height: 0 };
dimensions.widht = 3; // no error

that's an edge case though, and as @RyanCavanaugh said it's far more likely that assigning an unknown key is an error, which is why it wouldn't make sense to not show an error by default, expecting the user to use as const to opt into the functionality they'd want 99% of the time

@KotlinIsland
Copy link

@aaroncowie A subtype could already contain that key with an unrelated type, therefor losing type soundness.

function foo(a: { width: number, height: number }) {
    a.length = "idk"
}
const a = { width: 0, height: 0, length: 0 }
foo(a)
a.length // STRING????? HUUUHHH?????

@SlurpTheo
Copy link

@lazytype

function f<K extends string, T extends object>(key1: K, key2: K, genericObj: T, concreteObj: {foo: string}) {
  if (key1 in concreteObj) {
    concreteObj[key2]; // ts(2536): Type 'K' cannot be used to index type '{ foo: string }'.
  }

This is obviously unsafe, and there's no way in the type system to express that concreteObj has key1 but not key2.

I'm not following what's obvious about this issue/thread/example. It makes sense to me that you can't index by key2 as f<"foo" | "BAR", { foo: string }>("foo", "BAR", { foo: "a3" }, { foo: "a4" }) is a type-valid caller of this API.

@lazytype
Copy link

It makes sense to me that you can't index by key2

The goal of the comment I was replying to is to be able to index concreteObj by key1 after checking for key1's existence with the in operator. What is "obvious" is that it would be unsafe to index concreteObj by key2 if only checking for the existence of key1. My point is that from the perspective of the type system key1 and key2 are of the same type. So there's no way to model the behavior that concreteObj is indexable by key1 but not key2 in the current type system.

github-merge-queue bot pushed a commit to powerhome/playbook that referenced this issue Jan 22, 2024
https://nitro.powerhrg.com/runway/backlog_items/PLAY-1083

**What does this PR do?** A clear and concise description with your
runway ticket url.

**Task**
Run our new linter on kits that start with F - J.

**Because**
We've implemented a linter config, but most/all of our library files are
still failing its ruleset. We need to update all kits in groups to
eventually clean up the entire library to confirm with the new linter
configs.

**Note**

I also fixed as many warning as I could.

N/A

💻 `yarn run eslint --quiet
"./playbook/app/pb_kits/playbook/{pb_f,pb_g,pb_h,pb_i}*/**/*.{js,ts,tsx,jsx}"`

```
yarn run v1.22.15
$ /Users/stephen.marshall/code/playbook/node_modules/.bin/eslint --quiet './playbook/app/pb_kits/playbook/{pb_f,pb_g,pb_h,pb_i}*/**/*.{js,ts,tsx,jsx}'

/Users/stephen.marshall/code/playbook/playbook/app/pb_kits/playbook/pb_fixed_confirmation_toast/_fixed_confirmation_toast.tsx
  44:21  error  Unexpected empty arrow function                           @typescript-eslint/no-empty-function
  83:30  error  Prop `onClick` must be placed on a new line               react/jsx-max-props-per-line
  84:46  error  Prop `fixedWidth` must be placed on a new line            react/jsx-max-props-per-line
  90:17  error  Expected indentation of 18 space characters but found 16  react/jsx-indent-props
  91:17  error  Expected indentation of 18 space characters but found 16  react/jsx-indent-props
  92:17  error  Expected indentation of 18 space characters but found 16  react/jsx-indent-props
  98:39  error  Prop `cursor` must be placed on a new line                react/jsx-max-props-per-line

/Users/stephen.marshall/code/playbook/playbook/app/pb_kits/playbook/pb_flex/_flex.tsx
  10:10  error  Don't use `object` as a type. The `object` type is currently hard to use ([see this issue](microsoft/TypeScript#21732)).
Consider using `Record<string, unknown>` instead, as it allows you to more easily inspect and use the keys  @typescript-eslint/ban-types

/Users/stephen.marshall/code/playbook/playbook/app/pb_kits/playbook/pb_form_group/_form_group.tsx
  10:10  error  Don't use `object` as a type. The `object` type is currently hard to use ([see this issue](microsoft/TypeScript#21732)).
Consider using `Record<string, unknown>` instead, as it allows you to more easily inspect and use the keys  @typescript-eslint/ban-types

/Users/stephen.marshall/code/playbook/playbook/app/pb_kits/playbook/pb_form_pill/_form_pill.tsx
  34:21  error  Unexpected empty arrow function         @typescript-eslint/no-empty-function
  51:26  error  Prop `id` must be placed on a new line  react/jsx-max-props-per-line

/Users/stephen.marshall/code/playbook/playbook/app/pb_kits/playbook/pb_gauge/_gauge.tsx
   20:10  error  Don't use `Boolean` as a type. Use boolean instead      @typescript-eslint/ban-types
  193:7   error  Expected indentation of 8 space characters but found 6  react/jsx-indent-props
  200:7   error  Expected indentation of 8 space characters but found 6  react/jsx-indent-props
  201:7   error  Expected indentation of 8 space characters but found 6  react/jsx-indent-props

/Users/stephen.marshall/code/playbook/playbook/app/pb_kits/playbook/pb_home_address_street/_home_address_street.tsx
  66:7  error  Expected indentation of 8 space characters but found 6  react/jsx-indent-props
  67:7  error  Expected indentation of 8 space characters but found 6  react/jsx-indent-props
  68:7  error  Expected indentation of 8 space characters but found 6  react/jsx-indent-props
  69:7  error  Expected indentation of 8 space characters but found 6  react/jsx-indent-props

/Users/stephen.marshall/code/playbook/playbook/app/pb_kits/playbook/pb_icon_stat_value/_icon_stat_value.tsx
  16:10  error  Don't use `object` as a type. The `object` type is currently hard to use ([see this issue](microsoft/TypeScript#21732)).
Consider using `Record<string, unknown>` instead, as it allows you to more easily inspect and use the keys  @typescript-eslint/ban-types

/Users/stephen.marshall/code/playbook/playbook/app/pb_kits/playbook/pb_icon_value/_icon_value.tsx
  15:10  error  Don't use `object` as a type. The `object` type is currently hard to use ([see this issue](microsoft/TypeScript#21732)).
Consider using `Record<string, unknown>` instead, as it allows you to more easily inspect and use the keys  @typescript-eslint/ban-types

✖ 21 problems (21 errors, 0 warnings)
  15 errors and 0 warnings potentially fixable with the `--fix` option.

info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
```

#### Checklist:
- [x] **LABELS** Add a label: `enhancement`, `bug`, `improvement`, `new
kit`, `deprecated`, or `breaking`. See [Changelog &
Labels](https://github.com/powerhome/playbook/wiki/Changelog-&-Labels)
for details.
- [x] **DEPLOY** I have added the `milano` label to show I'm ready for a
review.
- [x] **TESTS** I have added test coverage to my code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Committed The team has roadmapped this issue Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Fix Available A PR has been opened for this issue Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet