-
-
Notifications
You must be signed in to change notification settings - Fork 3
Type Annotation for JavaScript Objects
⚠︎ Anti-pattern:
Use of a banned type detected JS-0296
As some standard built-in JavaScript objects are considered dangerous or harmful, some built-in types have aliases.
The practice of banning certain types is often a good idea to help improve consistency and safety.
Applies to: TypeScript
Reported by: TSLint
This rule bans specific types from being used. Does not ban the corresponding runtime objects from being used.
It takes a list of ["regex", "optional explanation here"]
, which bans types that match regex
.
This rule includes a set of "best practices" intended to provide safety and standardization in our codebase:
- Don't use the upper-case primitive types; lower-case types should be used for consistency:
-
Object
->object
-
Number
->number
-
String
->string
-
BigInt
->bigint
-
Symbol
->symbol
-
Boolean
->boolean
-
ⓧ Avoid the
object
type if able, as it's currently hard to use due to not being able to assert that keys exist.
Purposely select the lowercase object
type instead of the capitalized Object
type when:
- The variable in question being annotated doesn't inherit from
Object.prototype
^1
ⓘ Better still would be to define the object's shape, but if it can be anything
ⓘobject
is better thanany
ⓘ
unknown
is better thanany
⚠︎ You can assignany
values tounknown
type variables, but you cannot use them before doing a type check or type assertion.
⚠︎ Don't use
Object
as a type. Maybe use{}
instead? Object literal expressions should be populated, though.
⚠︎ Avoid the both
Object
and{}
types, as they mean "any non-nullish value".
ⓘ This is a point of confusion for many developers, who think it means "any object type".
- Avoid the
Function
type, as it provides little safety for the following reasons:- It provides no type safety when calling the value, which means it's easy to provide the wrong arguments
- It accepts class declarations, which will fail when called, as they are called without the
new
keyword
// use of upper-case primitives
const str: String = "foo";
const bool: Boolean = true;
const num: Number = 1;
const symb: Symbol = Symbol("foo");
// use a proper function type
const func: Function = () => 1;
// use safer object types
const lowerObj: object = {};
const capitalObj1: Object = 1;
const capitalObj2: Object = { a: "string" };
const curly1: {} = 1;
const curly2: {} = { a: "string" };
// use lower-case primitives for consistency
const str: string = "foo";
const bool: boolean = true;
const num: number = 1;
const symb: symbol = Symbol("foo");
// use a proper function type
const func: () => number = () => 1;
// use safer object types
const lowerObj: Record<string, unknown> = {};
const capitalObj1: number = 1;
const capitalObj2: { a: string } = { a: "string" };
const curly1: number = 1;
const curly2: Record<"a", string> = { a: "string" };
href="#improved-intersection-reduction-union-compatibility-and-narrowing" class="linkicon" aria-labelledby="improved-intersection-reduction-union-compatibility-and-narrowing">
TypeScript 4.8 brings a series of correctness and consistency
improvements under --strictNullChecks
. These changes affect how
intersection and union types work, and are leveraged in how TypeScript
narrows types.
For example, unknown
is close in spirit to the union type
{} | null | undefined
because it accepts null
, undefined
, and any
other type. TypeScript now recognizes this, and allows assignments from
unknown
to {} | null | undefined
.
function f(x: unknown, y: {} | null | undefined) {
x = y; // always worked
y = x; // used to error, now works
}
Another change is that {}
intersected with any other object type
simplifies right down to that object type. That meant that we were able
to rewrite NonNullable
to just use an intersection with {}
, because
{} & null
and {} & undefined
just get tossed away.
- type NonNullable<T> = T extends null | undefined ? never : T;
+ type NonNullable<T> = T & {};
This is an improvement because intersection types like this can be
reduced and assigned to, while conditional types currently cannot. So
NonNullable<NonNullable<T>>
now simplifies at least to
NonNullable<T>
, whereas it didn’t before.
function foo<T>(x: NonNullable<T>, y: NonNullable<NonNullable<T>>) {
x = y; // always worked
y = x; // used to error, now works
}
These changes also allowed us to bring in sensible improvements in
control flow analysis and type narrowing. For example, unknown
is now
narrowed just like {} | null | undefined
in truthy branches.
function narrowUnknownishUnion(x: {} | null | undefined) {
if (x) {
x; // {}
}
else {
x; // {} | null | undefined
}
}
function narrowUnknown(x: unknown) {
if (x) {
x; // used to be 'unknown', now '{}'
}
else {
x; // unknown
}
}
Generic values also get narrowed similarly. When checking that a value
isn’t null
or undefined
, TypeScript now just intersects it with {}
— which, again, is the same as saying it’s NonNullable
. Putting many of
the changes here together, we can now define the following function
without any type assertions.
function throwIfNullable<T>(value: T): NonNullable<T> {
if (value === undefined || value === null) {
throw Error("Nullable value!");
}
// Used to fail because 'T' was not assignable to 'NonNullable<T>'.
// Now narrows to 'T & {}' and succeeds because that's just 'NonNullable<T>'.
return value;
}
value
now gets narrowed to T & {}
, and is now identical with
NonNullable<T>
– so the body of the function just works with no
TypeScript-specific syntax.
On their own, these changes may appear small — but they represent fixes for many, many paper cuts that have been reported over several years.
For more specifics on these improvements, you can read more here.