rareyesdev / blog Goto Github PK
View Code? Open in Web Editor NEWHome Page: https://www.rareyes.dev
Home Page: https://www.rareyes.dev
When working in FlowJS codebases I've found that there is no standard way of typing the component. These are some of the patterns I've encountered:
function MyComponent(props: Props) {
return <>JSX here</>
}
const MyComponent = (props: Props) => {
return <>JSX here</>
}
In both cases, only the input is being typed. Nothing prevents us from making mistakes and returning something that is not a ReactElement
.
We can consider enforcing a consistent way of defining a React Component in TypeScript like this:
import { FC } from 'react';
interface Props {
name: string;
}
const MyComponent: FC<Props> = ({ name }) => {
return <div>Hello {name}</div>
};
This allows for type-safe access to the props and ensures that we always return valid React Elements.
As a bonus, I recommend you configure @typescript-eslint/ban-types
to ensure a single way to import the component type definition (example ESLint Config).
"@typescript-eslint/ban-types": [
"error",
{
types: {
"React.FC": {
message: "Use FC instead",
fixWith: "FC",
},
"React.FunctionComponent": {
message: "Use FC instead",
fixWith: "FC",
},
FunctionComponent: {
message: "Use FC instead",
fixWith: "FC",
},
},
extendDefaults: true,
},
]
The type definitions for react@17 implicitly add an optional children
prop to each component. This means that you can destructure children without having to define it in the component props. the team behind @types/react decided to finally remove this implicit prop after years of consideration (this has nothing to do with react@18 features, Semantic Versioning works different for type packages, you can read more here)
The important thing to note is that after upgrading to react@18 you might encounter errors when a component uses its children
prop without explicitly declaring it.
Declaring a children
prop is often difficult for developers who sometimes confuse ReactNode
with ReactElement
(not mentioning the fortunately deprecated types ReactText
and ReactChild
). For this reason, it's encouraged that children
props be defined like this:
import { FC, PropsWithChildren } from 'react';
const MyComponent: FC<PropsWithChildren<Props>> = ({ children, name }) => {
return (
<div>
<div>Hello {name}</div>
<div>{children}</div>
</div>
);
};
This will add the children
prop back with the correct type.
In the scenario in which we need to do some conditional logic based on a discriminant value we see code like this:
calculateRideFare
,calculateDeliveryFare
, andcalculateDeliveryFare2
are just fake methods that simulate that we are doing different things on each branch.
const calculateFare1 = (product: Product) => {
if (product.type === 'RIDE') {
const rideFare = 0; // API call to get the fare.
return calculateRideFare(product, rideFare);
} else if (product.type === 'FOOD_DELIVERY') {
const deliveryFare = 0; // API call to get the fare.
return calculateDeliveryFare(product, deliveryFare);
} else {
const deliveryFare = 0; // API call to get the fare.
return calculateDeliveryFare2(product, deliveryFare);
}
};
const calculateFare2 = (product: Product) => {
if (product.type === 'RIDE') {
const rideFare = 0; // API call to get the fare.
return calculateRideFare(product, rideFare);
} else if (product.type === 'FOOD_DELIVERY') {
const deliveryFare = 0; // API call to get the fare.
return calculateDeliveryFare(product, deliveryFare);
} else if (product.type === 'ITEM_DELIVERY') {
const deliveryFare = 0; // API call to get the fare.
return calculateDeliveryFare2(product, deliveryFare);
}
throw new Error(`Unhandled product type ${product.type}`);
};
const calculateFare3 = (product: Product) => {
switch (product.type) {
case 'RIDE': {
const rideFare = 0; // API call to get the fare.
return calculateRideFare(product, rideFare);
}
case 'FOOD_DELIVERY': {
const deliveryFare = 0; // API call to get the fare.
return calculateDeliveryFare(product, deliveryFare);
}
case 'ITEM_DELIVERY': {
const deliveryFare = 0; // API call to get the fare.
return calculateDeliveryFare2(product, deliveryFare);
}
default:
throw new Error(`Unhandled product type ${product.type}`);
}
};
Option 1 is pretty bad, there is nothing set in place to alert us that some code needs to be updated. In cases like this we have to start hunting product.type
all around. Because of the hight frequency of this kind of conditional logic this is the root of many bugs.
Option 2 and 3 make it a bit easier by throwing an error that will (hopefully) be caught during development as we implement the happy path. There is still a chance that a non happy path stays outdated (even more if the type is used for more than one application).
Ideally, we want to set some restriction around each use of this discriminant logic so that a new possible value will trigger a type error anywhere the value is being conditionally tested.
We can archive this in TypeScript using an exhaustive switch like this:
const assertUnreachable = (value: never): never => {
throw new Error(`No such case in exhaustive switch: ${value}`);
}
const calculateFare4 = (product: Product) => {
switch (product.type) {
case 'RIDE': {
const rideFare = 0; // API call to get the fare.
return calculateRideFare(product, rideFare);
}
case 'FOOD_DELIVERY': {
const deliveryFare = 0; // API call to get the fare.
return calculateDeliveryFare(product, deliveryFare);
}
case 'ITEM_DELIVERY': {
const deliveryFare = 0; // API call to get the fare.
return calculateDeliveryFare2(product, deliveryFare);
}
default:
return assertUnreachable(product.type);
}
};
When a new type
is added TypeScript will complain everywhere and we can easily add new logic to handle the new product type. I was not able to do something similar when using FlowJS.
Another nice benefit of consistently using switch for these cases is that we can easily group logic that requires the same handling:
const calculateFare4 = (product: Product) => {
switch (product.type) {
case 'RIDE': {
const rideFare = 0; // API call to get the fare.
return calculateRideFare(product, rideFare);
}
case 'FOOD_DELIVERY':
case 'ITEM_DELIVERY': {
const deliveryFare = 0; // API call to get the fare.
return calculateDeliveryFare(product, deliveryFare);
}
default:
return assertUnreachable(product.type);
}
};
You can read more about the
never
type here
switch
with TS Config - No implicit returnsThese solutions are less flexible and may not be available depending on the project configuration. For this reason, Option 4 is a very good pattern to standardize in the codebase.
Enums are available in both Flow and TypeScript. Codebases may also use complimentary techniques like keymirror.
Issues with keymirror:
In both type systems, using enums has several drawbacks.
In many cases, String Literals are a better tool for the usual Enum use case. You can read more about this here.
One additional way to use String Literals not described in the article above is how to use String Literals when the list of values is also needed.
Consider an enum for a log level:
enum LogLevel {
INFO = "INFO",
WARNING = "WARNING",
ERROR = "ERROR",
}
Maybe some code needs to go through all the keys and so something with it (for example show a dropdown with all possible values):
const dropdownValues = Object.values(LogLevel).map((value) => ({ label: translate(value), value: key }));
This is often repeated many times in the code with potential issues. A common mistake is to confuse keys and values when iterating over the object. Trying to build the dropdown options using Object.values(...)
will implicitly assume keys and values will always be the same. This bug could be caught by the type system only if the usage is also typed properly (translate
function in this case).
As an alternative, we can define a String Literal like this:
const LogLevels = [
"INFO",
"WARNING",
"ERROR",
] as const;
type LogLevel = typeof LogLevels[number];
This way you define the keys only once and keep the codebase free of duplicated code (example in TypeScript Playground).
Consider creating a bookmarks page.
Useful links:
Use jest.mocked
utility if you are on jest >= 27.4.4 else use [email protected] (version matters because they deleted the utility after it as moved to Jest).
In Flow there are multiple ways to express that a value is not required:
This accepts either no value to be passed or a value whose type that extends void
.
type Rider = {|
phoneNumber?: PhoneNumber;
|};
const getValue = (x?: string) => {...}
This requires a value to be passed but the value's type can extend void | null
.
type Rider = {|
phoneNumber: ?PhoneNumber;
|};
const getValue = (x: ?string) => {...}
In TypeScript case (1) uses the same syntax while case (2) needs a longer more explicit syntax:
type Rider = {|
phoneNumber: PhoneNumber | null | undefined;
|};
const getValue = (x: string | null | undefined) => {...}
When migrating from Flow to TypeScript, consider using (1) and avoiding (2) entirely. This recommendation is based on You don't need null. Use undefined instead. Please refer to the linked article to understand the rationale behind this recommendation.
Many backend services generate weak IDL types in which most properties are "double optional". As a result, the generated Flow and TypeScript definitions are bad.
Don't leak those weak types into your code. Create an adaptor layer in your RPC handlers or GraphQL resolvers to map to better types. You can read how the Guest Rides team does this in the project documentation: How to map from backend to frontend types?.
Optional will exist in your code but you should try to minimize them. You should also minimize the places in which a value must be checked for presence.
For those few places in which you need to check, use an assertion function.
You don't need
null
. Useundefined
instead.
In that case, just assign undefined to it:
const anObject = {
...otherObject,
propertyToNullify: undefined,
};
Folks that use it a lot (generally coming from other languages that have null as the only nullish value) get pretty mad about such claims. The most common response I get is:
null is for intentional missing values, and undefined should be used when the values were never set in the first place.
The first thing I think with responses like that is: Why would you ever need to make that distinction? Both are "nullish", and you don't need to differentiate between "intentionally missing" and "unintentionally missing".
const people = [
{
firstName: "Luke",
middleName: null,
lastName: "Shiru",
},
{
firstName: "Barack",
middleName: "Hussein",
lastName: "Obama",
},
];
But you can just omit middleName
when the user doesn't have one:
const people = [
{
firstName: "Luke",
lastName: "Shiru",
},
// ...
];
And you can set middleName
to an empty string if the user intentionally left that blank, if you really need to know that for some reason:
const people = [
{
firstName: "Luke",
middleName: "",
lastName: "Shiru",
},
// ...
];
And the TypeScript representation would be something like this:
type Person = {
firstName: string;
middleName?: string;
lastName: string;
};
Yes, it does, and it's a pain, but you can easily type that like this instead:
const element = document.querySelector('wrong-selector') ?? undefined;
The nullish coalescing operator ??
can be used to convert from null to undefined anywhere a null value is received from an external API.
In JSON structures undefined cannot be used as that type does not exist. You can hover assign null to a property {"someProperty": null}
, but why would you do that? If the property someProperty
has no value, it's better to just exclude that property completely. Keeping such properties just increases the payload size that needs to be sent over the network or stored in a file without actually giving you any extra benefits.
If your code base interacts with other APIs that might give you a null the use an ==
check instead of ===
. value == undefined
will be true for null
and undefined
but not for other falsy values (''
, false
, 0
).
/// Imaging you are doing `value == undefined` where value can be one of:
console.log(undefined == undefined); // true
console.log(null == undefined); // true
console.log(0 == undefined); // false
console.log('' == undefined); // false
console.log(false == undefined); // false
Just don’t set it. Your type annotation should have it as optional anyways. Don’t set it to null
or undefined
. Just have it as absent:
interface Foo {
a: number;
b?: number;
}
// Don't set `b` to anything.
const foo: Foo = {
a: 123
};
Setting any array to undefined will trigger presence checks down the line. Initialize all arrays to empty instead.
That is not a nice thought, anyhow, ask them nicely and then add the eslint-plugin-no-null plugin to your .eslintrc:
{
"plugins": [
"no-null"
],
"rules": {
"no-null/no-null": 2
}
}
Never use null, unless the ecosystem dictates it.
e.g. node style callbacks expect the error argument to be set to null in case of no error. Even then I (and pretty much everyone else) do a truthy check on null and not an explicit one.
fs.readFile('someFile', 'utf8', (err,data) => {
if (err) {
// do something
}
// no error
});
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.