typescript type safety typescript types typescript type guards

Typescript: some things you might want to know if you just started

Typescript strength resides to it adding typing to javascript. You will find in this articles some tricks and tips to better use it.

Typescript: type predicate function example.

Typescript: type predicate function example.

As a reminder, typescript is a superset of JavaScript and was developed by Microsoft to add static typing (it also add some other features but we won’t be talking about it in this article).

Why is it interesting? By adding static typing you can have a more robust code where you know what each variable should contain. While some might argue that writing a software using typescript takes more time than using plain JavaScript, we have to be more nuanced as a TypeScript program is less likely to have runtime bugs than a plain JavaScript and so will require less time in debugging.

Typescript is never directly interpreted by a computer, it must be first converted to javascript (it is a transpilation: a translation from one language to another)

Typing mistakes

“As any” isn’t your friend

When you wrote as any in your code, you explicitly disable type checking, hence you remove the benefits from using TypeScript and prevent it from detecting issues at transpilation time.

For linters, using any is considered as an error!

When creating objects, it is safer to indicate their type beforehand instead of casting them.

Let’s say you have declared a type with some optional properties you might not know beforehand and a function using it.

type Point {  x: number;  y: number;  z?: number; // optional}function someFunction(point: Point) {}

You might want to instantiate directly an object and use it straight away:

const myPoint = {  x: 0,  y: -1,}someFunction(myPoint);

This will work.

The issue is that you won’t be able to add the z property to myPoint after the initial declaration.

const myPoint = {  x: 1,  y: 2,}// TS2339: Property z does not exist on type { x: number; y: number; }myPoint.z = 3;

Why? Because without further information typescript assumed the myPoint variable to be of type { x: number; y: number; }

Now if you want to declare directly myPoint as a Point value, you might use one of the following approaches

// version 1, it will check that the object value overlap the Point type (it could have more properties)const myPoint = {...} as Point;// version 2, it will check that the object value is indeed of Point type and will// refuse to have more propertiesconst myPoint: Point = {}

Of course I would recommend you to use the second version as it is safer.

Utility types are overwhelming

You can take a look at the official typescript documentation page.

Utility types are used to infer a type using another one, mostly useful when the second type comes from a library.

The ones I am particularly found of are:

  • Awaited, mostly in combination with ReturnType
  • Partial
  • Record<Keys, Type>
  • Pick<Type, Keys> Omit<Type, Keys>
  • NonNullable

ReturnType: given a method or a function, you can use it to get the expected return type

type ReturnValue = ReturnType<typeof someFunction>;

Awaited: extract the success value type from a promise

type FinalValue = Awaited<ReturnValue>;

Mixed together they can be powerful when working with functions from external libraries :

// someAsyncFunction is imported from a librarytype FinalValue = Awaited<ReturnType<typeof someAsyncFunction>>;const value: FinalValue = await someAsyncFunction()

I rarely had to use them, but the few times I did had to use them they helped me gain lot of times and safety.


Partial: will create a copy type where all properties are optional.

type OriginalType = {  x: string;  y: string;};type PartialType = Partial<OriginalType>;// Is equivalent to writing// PartialType = {//   x?: string;//   y?: string;// }

Record: useful to create associative map where no keys are left behind.

type Themes = 'light' | 'dark' | 'auto';// > TS2741: Property auto is missing in type { light: string; dark: string; } but required in type Record<Themes, string>const NamedThemes: Record<Themes, string> = {  light: 'Light mode',  dark: 'Dark mode',}

Pick/Omit: to create a type by picking/omitting the properties of another one.

type Point3D = {  x: number;  y: number;  z: number;}// creating a new type by selecting properties from the original typetype Point2DPick = Pick<Point3D, 'x' | 'y'>;// creating a new type by omiting properties from the original typetype Point2DOmit = Omit<Point3D, 'z'>;// for the record, but better use this kind of construction // when mixing properties from different typestype Point2DVerbose = {  x: Point3D['x'];  y: Point3D['y'];}

NonNullable: to remove null / undefined possibilities from type

type SimpleOptionalString = string | undefined | null;type NewType = NonNullable<SimpleOptionalString>; // stringtype ObjectOptionalValues = {  keyA: string | undefined;  keyB: number | null;}type NewTypeB = NonNullable<ObjectOptionalValues>;// type NewTypeB = {//   keyA: string;//   keyB: number;// }

Type Guards

Type guards is a powerful way to check the specific type of a value when there is an ambiguity.

The example case: describing fields

Let’s say we describe fields in a generic way. In the example we can have descriptions for text and integer fields.

type TextField = {  type: 'text';  defaultValue: string;  minLength?: number;  maxLength?: number;}type IntField = {  type: 'int';  defaultValue: number;  minValue?: number;  maxValue?: number;}// other field declarations...type GenericField = TextField | IntField | ...;

We have a function (or a React component) that should be able to handle both those cases.

Without a type guard we should use an explicit cast operation as checking the value of the field type is not enough for typescript to fully assume an object of being of a type.

function textFieldHandler(field: TextField) {  // do something}function intFieldHandler(field: IntField) {  // do something else}function genericFieldHandler(field: GenericField) {  const type = field.type;  if (type === 'text') {    // field should still be casted!    // field.minLength would cause an error during transpilation    textFieldHandler(field as TextField);  } else if (type === 'int') {    // field should still be casted!    // field.minValue would cause an error during transpilation    intFieldHandler(field as IntField);  }}

By creating type predicates we can both check the type and implicitly cast the value.

function isText(field: GenericField): field is TextField {  return field.type === 'text';}function isInteger(field: GenericField): field is IntField {  return field.type === 'int';}function genericFieldHandler(field: GenericField) {  if (isText(field)) {    // field is implicitly casted as TextField    // field.minLength would NOT cause an error during transpilation    textFieldHandler(field);  } else if (isInteger(field)) {    // field is implicitly casted as IntField    // field.minValue would NOT cause an error during transpilation    intFieldHandler(field);  }}

In the given examples the predicates are very simples, just checking the value of a single field. When ambiguity is higher, do not hesitate to perform more rigorous tests and verify all expected properties.

Final words:

In the era of AI-generated code, it is becoming even more important to have a thorough understanding of the languages we use.
We must not only write code that compiles, but also ensure that it will function correctly in production. Strong typing is a first guarantee, even if it is not sufficient on its own.