DEV Community

Cover image for The types in TypeScript
Chris Bongers
Chris Bongers

Posted on • Originally published at daily-dev-tips.com

The types in TypeScript

When it comes to TypeScript, a big part of the game is defining types.

With this, we can define annotations, but they can appear in more places.

In this specific article, we will go through the most basic types, and eventually, we'll dive a bit deeper into extended kinds.

The pillar of types

There are the primitive types that are very commonly used in JavaScript, basically responsible for most of your variables, and these three are:

  1. string: A string value
  2. number: A integer/number value, JavaScript doesn't care if it's an int or float. They call it a number
  3. boolean: The good old true or false

Besides these three pillars, you might need an array of certain elements.

Let's say an array of strings. We can use the bracket annotation for that: string[].

A tale of caution

When it comes to TypeScript, the default type will be used if you don't define something in particular.
This type is called any, and it could be anything.

You want to avoid using the any type when defining types.
You can even set the noImplicitAny flag to throw errors if any is used.

Using the types

Whenever you declare a variable or function, you can annotate the type by using a : {type} format.

Let's see how it would look for a variable and function:

let username: string = 'Chris';

const myName = (name: string) => {
  console.log(`Hello ${name}`);
};
Enter fullscreen mode Exit fullscreen mode

However, note that we don't explicitly have to mention a type on the' username' variable.
This is because TypeScript is smart enough to derive this as a string.

Let me show you what I mean by that:

TypeScript auto type

In the image above, you can see that we set the value as a string on the left and the right as a number.

Without explicitly telling a type, TypeScript knows what is going on.
This is only possible with variables that have a direct value!

We can also define the return type for functions.
We have a function that takes a number but returns a string.

const numberToString = (number: number): string => {
  return number.toString();
};

const output = numberToString(123);
Enter fullscreen mode Exit fullscreen mode

Note the : string behind the function, which is used to define a function's return type.

We already had a brief look at the array type. Another side pillar is the object annotation, defined by curly brackets.

const getFullName = (user: {firstname: string, lastname: string}): string => {
  return `${user.firstname} ${user.lastname}`;
};

getFullName({firstname: 'Chris', lastname: 'Bongers'});
Enter fullscreen mode Exit fullscreen mode

In the above example, the function accepts an object as the user variable. This object has two properties which both are strings.

Making types optional

Let's take the above example. There might be cases where we only know the first name and still want to call this function.
In our current implementation, it will throw a TypeScript error.

Type is missing

You can see that TypeScript states we are missing a required type of the last name.

We can prefix the : with a question mark to make a type optional.

const getFullName = (user: {firstname: string, lastname?: string}): string => {
  return `${user.firstname} ${user.lastname}`;
};
Enter fullscreen mode Exit fullscreen mode

It's important to note that by default, variables are required. We must explicitly mention which ones are optional.

What if my variable has multiple types?

This happens more often. Let's take an ID. For example, it could be a number or a string.

To define a type that has multiple, we have to use the union type.
You can define these union types using the pipe | option.

const getUserId = (id: number | string) => {
  return `Your ID is ${id}`;
};

getUserId(123);
getUserId('Chris123');
Enter fullscreen mode Exit fullscreen mode

As you can see, both use-cases are now valid.

However, what if we need to use a particular function that's not valid for one of the two?

We want to prefix the number IDs with a batch prefix, but the string versions already have this:

const getBatchString = (id: number | string): string => {
  if (typeof id === 'number') {
    id = `batch-${id}`;
  }
  return id;
};

getBatchString(123);
getBatchString('batch-123');
Enter fullscreen mode Exit fullscreen mode

In the above example, you can see that we can use typeof to determine which one of the two it is.

In the case of a number, we prefix it with a string. Otherwise, we return the string.

Both these use-cases will return batch-123.

And that's it for the basic types of TypeScript and how we can use them.

Thank you for reading, and let's connect!

Thank you for reading my blog. Feel free to subscribe to my email newsletter and connect on Facebook or Twitter

Top comments (16)

Collapse
 
peerreynders profile image
peerreynders • Edited

The main difference is that we don't type the return value, because is inferred.

I'm fully aware that inferred function return types are considered idiomatic in TypeScript. In the end though I think that simply reflects an attitude where convenience wins over type safety (i.e. the popularity of TypeScript + VS Code is much less about type safety but much more about convenience).


Google TypeScript Style Guide: Return types

Whether to include return type annotations for functions and methods is up to the code author. Reviewers may ask for annotations to clarify complex return types that are hard to understand. Projects may have a local policy to always require return types, but this is not a general TypeScript style requirement.

There are two benefits to explicitly typing out the implicit return values of functions and methods:

  • More precise documentation to benefit readers of the code.
  • Surface potential type errors faster in the future if there are code changes that change the return type of the function.

One minor point is the fact that functions have a type. One of the more infuriating things is that you cannot type a function declaration with a function type - one has to use arrow functions for that. So by not explicitly specifying the return type half of the function's type is left unspecified/implicit. Sure, an IDE will be able to show that type but that means that now even code reviews have to be conducted in the presence of "assistive technologies" (as the explicit code simply doesn't tell the whole story).

The major point however:

A Gentle Introduction to haskell Version 98 - 2.2.1 Recursive Types
"For example, suppose we wish to define a function fringe that returns a list of all the elements in the leaves of a tree from left to right. It's usually helpful to write down the type of new functions first"

The return type of a function is part of its design, its contract so it makes sense to explicitly determine and state it even before the body is implemented. So if one is already thinking in types and is perhaps even practicing type driven development then it makes sense to explicitly capture the types in play, perhaps even adding "more boilerplate" by adding type aliases that give the types more descriptive and meaningful names.

An implicit return type projects an attitude that the return type is merely an accidental byproduct of the function's implementation - while types are mandatory for the inputs (arguments);

// Inferred return type is ("100" | 1)
// Is that the intent?
// A union of a string literal and a numeric literal?
//
const fn = (arg: number) => arg > 50 ? '100' : 1;
Enter fullscreen mode Exit fullscreen mode

It's one thing to rely on type inference within the confines of a function body - but crossing the function (or method) boundary is significant enough to warrant an explicit type check/lint (even for one liners) in order to catch problems as close to the source as possible.

Does that mean more typing on the keyboard? Sure; not enough though to affect productivity.


Interestingly Effective Typescript has "Item 19: Avoid Cluttering Your Code with Inferable Types" (p.81) which unsurprisingly states:

"The explicit type annotation is redundant. Writing it just adds noise. If you’re unsure of the type, you can check it in your editor."

Only to later clarify (p.85):

"Similar considerations apply to a function’s return type. You may still want to annotate this even when it can be inferred to ensure that implementation errors don’t leak out into uses of the function."

The item is summarized (p.87) with:

  • Avoid writing type annotations when TypeScript can infer the same type.
  • Ideally your code has type annotations in function/method signatures but not on local variables in their bodies.
  • Consider using explicit annotations for object literals and function return types even when they can be inferred. This will help prevent implementation errors from surfacing in user code.
Collapse
 
snigo profile image
Igor Snitkin

You don't need to go into an advanced discussion to see why return type annotation is useful. What if it's 7am, you haven't got your coffee yet and you make a very simple typo as a result?

const numberToString = (number: number) => {
    return number.toString;
};
Enter fullscreen mode Exit fullscreen mode

Return type annotation will catch it, isn't it?

 
peerreynders profile image
peerreynders

Inferring the return type of a function is not an "accidental byproduct", but actually the result of your arguments. Depending on what you do with those arguments, the type of the return.

the value I see in inferred return types is that you can change the implementation and get the correct "new type" without having to go and change the output type every time you do this.

So again the return type is a consequence of the implementation.

When you "think in/develop with types" the implementation is a consequence of the desired type - i.e. the relationship it flipped.

Consider this scenario

type MyType = {
  count: number;
  average: number;
};

function fn(values: number[]) {
  const count = values.length;
  const average =
    count > 0 ? values.reduce((sum, value) => sum + value, 0) / count : NaN;

  return {
    count,
    average,
  };
}
Enter fullscreen mode Exit fullscreen mode

We find that MyType also needs total.

type MyType = {
  count: number;
  average: number;
  total: number;
};

function fn(values: number[]) {
  const count = values.length;
  const average =
    count > 0 ? values.reduce((sum, value) => sum + value, 0) / count : NaN;

  return {
    count,
    average,
  };
}
Enter fullscreen mode Exit fullscreen mode

No error that the result from fn is no longer sufficient. If we are lucky somewhere the result will be delivered to an explicitly typed binding, alerting us to the problem with the function (or any others that produce a similar result).

With an explicit return type compilation will identify all the functions that no longer produce the correct type.

type MyType = {
  count: number;
  average: number;
  total: number;
};

function fn(values: number[]): MyType {
  const count = values.length;
  const average =
    count > 0 ? values.reduce((sum, value) => sum + value, 0) / count : NaN;

  // Property 'total' is missing in type '{ count: number; average: number; }' but required in type 'MyType'.
  return {
    count,
    average,
  };
}
Enter fullscreen mode Exit fullscreen mode

i.e. the type is changed before the implementation(s).

A capability in terms of value-orientation:

  • What is the shape of the data I need
  • What is that shape of the data I have
  • How do I transform what I have into what I need.

i.e. contracts (constraints) first, implementation last.

Aside Why type-first development matters

It looks like the intent of that code is to take a number argument and return "100" or 1, so yes, that the correct type for its output.

My intent was to simulate a defect (which perhaps would only be detected at an explicitly typed site of use), i.e. the intended return type should either have been string or number - not string | number.

This way you're effectively doing "JS with types" instead of writing a plethora of types just to write "more TS".

JS Doc TS with (hand written) declaration files is "Typed JS". There's a clean separation between "type space" (TypeScript types) and "value space" (the JS implementation).

The entire point of the index.d.ts and internal.d.ts files is to explicitly formulate a constrained set of types that are expected to be encountered (without having to formulate them "ad hoc").

The problem is that using JS Doc TS well (as far as I can tell) requires a higher level of TS competence (especially its "typing language") than writing TS destined for transpilation.

and many other utils to infer types from static values.

Those features can help to cut down on verbosity and emerge as TS tries to tease whatever type information it can out of the JS value space to make it available in type space. But in terms of design, types are typically formulated before the implementations and defined in type space (rather than extracted from value space).

Type space derived from value space is like drafting a blueprint after the house has already been built.

 
peerreynders profile image
peerreynders • Edited

I think your approach is heavily inspired by how folks solved stuff with classes.

Design by Contract is attributed to Bertrand Meyer in connection with the Eiffel OOPL (1986) but the "type first" notion appears elsewhere like in Clojure's "thinking in data, modeling in data, and working in data" which is most definitely not OO.

And the idea of "function types as contracts" is up-to-date - 3.7.1 Types and Contracts.

Explicit types serve as deliberate "check points" during type checking/linting.

Is faster because we focus on the implementation, and the types are derived from that.

Expedience is always fielded as the primary motivation for "implementation first, contract last" approaches. But more often than not, contract last approaches save you time in the short term but create problems (that could be avoided by actively managing your types) in the long term (TTIS - Time-To-Initial-Success).

To some degree it's like saying "I know that JSDoc annotations are helpful for maintenance but keeping them in sync with the code slows me down so it's not worth it". Types are part of the application's specification that are supposed make it easier to reason about the code, so it helps if they are not buried somewhere in the implementation.

I guess the lesson here is: just because TypeScript is being used doesn't actually reveal whether the potential of types is being leveraged to maximum effect (to the extend that is even possible in TypeScript).

Aside:

Collapse
 
lexlohr profile image
Alex Lohr

Slightly erroneous wording: types are inferred, not derived. Also readonly properties are missing here, even if you only want to show basic types and leave generics and advanced types for later.

Collapse
 
dailydevtips1 profile image
Chris Bongers

Fully aware and thanks for raising this.
However in the big scheme of showing these basics to beginners in TypeScript I think it's a valid explanation.

We don't always want to make things the neatest when explaining certain topics.

For instance your example with return values is super valid, but if we don't explain the options how will people know about it, when they do need it?

Besides I did note in the article you don't need to explicitly name variables that are assigned already.

Anyway, love the debate going on here, but please keep in mind these articles are to show a very basic starting point to TypeScript.

 
snigo profile image
Igor Snitkin

What about this place ;)

<div>{numberToString(42)}</div>
Enter fullscreen mode Exit fullscreen mode
 
snigo profile image
Igor Snitkin • Edited

you're passing a function to a ReactNode, which can be a function

Exactly, but isn't this fact making situation worse?

 
dailydevtips1 profile image
Chris Bongers

Love this point!

I split up my TypeScript into several articles, so might just focus one whole article on this point

Collapse
 
siy profile image
Sergiy Yevtushenko

Return type is part of the context, preserving it helps reading the code.