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 (30)

Collapse
 
lukeshiru profile image
Luke Shiru

I might create a post about this because I have to say it in every TypeScript post like this, but the best use of TypeScript is when you use it only when needed, and leave everything else for it to be inferred. For example:

// This function:
const numberToString = (number: number): string => {
    return number.toString();
};
// Can actually be written like this instead:
const numberToString = (number: number) => number.toString(10);
Enter fullscreen mode Exit fullscreen mode

The main difference is that we don't type the return value, because is inferred, same applies to this:

const getBatchString = (id: number | string): string => {
    if (typeof id === "number") {
        id = `batch-${id}`;
    }
    return id;
};
// vs
const getBatchString = (id: number | string) =>
    typeof id === "number" ? `batch-${id}` : id;
Enter fullscreen mode Exit fullscreen mode

Again, the type of the return is inferred. You can even make it infer the type of the arguments if you give them default values:

const add = (a: number, b: number): number => a + b;
// vs
const add = (a = 0, b = 0) => a + b;
Enter fullscreen mode Exit fullscreen mode

This doesn't apply only to functions, variables with a default value will be inferred as the type of that value, so...

// Instead of doing this
const username: string = "Chris";
// You just...
const username = "Chris";
Enter fullscreen mode Exit fullscreen mode

You don't need to type everything in order to make good use of TS, just the parts it can infer.

Cheers!

Collapse
 
peerreynders profile image
peerreynders • Edited on

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
 
lukeshiru profile image
Luke Shiru

TypeScript + VS Code is much less about type safety but much more about convenience

TypeScript is about convenience. If it gets in the way instead of helping, you might be using it wrong. I've been using TypeScript since it first became public (almost 10 years ago), and my advice when using TS is generally always the same: Just use it when it can't figure out types by itself, or when it really helps readability. Using types just for the sake of using them is not that useful, because the idea of TS is that is just "JavaScript with types on top". Like using JSDocs directly in the language.

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);

I beg to differ. 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. You can be explicit about the type if it helps readability or if you want to make sure you get an error if the operation you do in that function doesn't return what you expect, but it isn't a requirement. You can also create a type for the entire function in a "haskell like" kind of a approach:

type MathFunction = (value2: number) => (value1: number) => number;

const add: MathFunction = value2 => value1 => value1 + value2;
const subtract: MathFunction = value2 => value1 => value1 - value2;
const multiply: MathFunction = value2 => value1 => value1 * value2;
const divide: MathFunction = value2 => value1 => value1 / value2;
Enter fullscreen mode Exit fullscreen mode

But 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. And you still get all the type safety of TS, so if you were returning a number and now you return a string, you'll get the expected errors in all the places were you are using the output of that function as a number.

Your example looks like this:

// 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 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. This will work in places were we expect number | string, so no need to type the output like that.

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

Trust me, my advice has nothing to do with "typing less". I cringe when I see that folks use generics like T, or identifiers like e, so no, is not to type less. As I pointed out before, my advices is to use TS when actually needed, when it can't figure out stuff by itself. This way you're effectively doing "JS with types" instead of writing a plethora of types just to write "more TS".

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.

We have other options for that as well, you can for example make use of as const, and typeof, ReturnType and many other utils to infer types from static values.

const example1 = () => [1, 2]; // Return type is `number[]`
const example2 = () => [1, 2] as const; // Return type is `readonly [1, 2]`

const tuple = example2();

const example3 = (tupleArgument: typeof tuple) => tuple; // tupleArgument type is `readonly [1, 2]`
const example4 = (tupleArgument: ReturnType<typeof example2>) => tuple; // tupleArgument type is `readonly [1, 2]`
Enter fullscreen mode Exit fullscreen mode

I agree with you that we need to type the return when it helps readability, but when it doesn't, using the inferred type is more than ok and will help your code stay close the the JS it outputs, while still being type safe.

Thread Thread
 
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.

Thread Thread
 
lukeshiru profile image
Luke Shiru

I think your approach is heavily inspired by how folks solved stuff with classes. What I mean by this is that you think that types are like the design/blueprint of how everything should look like, way before you even start working on the implementation. Meanwhile, I prefer to use types in TypeScript as a tool to know what the type of something will be while I work on it, not before.

Considering your scenario, you could get that MyType from the function if you want:

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

    return {
        count,
        average,
    };
}

type MyType = ReturnType<typeof fn>;
Enter fullscreen mode Exit fullscreen mode

So if you change the implementation, the type adjusts to that. Basically, you make TS work for JS instead of the other way around. With this approach, let's say we have other two functions that take the output of fn as input:

const doubleCount = ({ count }: MyType) => count * 2;
const doubleAverage = ({ average }: MyType) => average * 2;
Enter fullscreen mode Exit fullscreen mode

With your approach, if let's say count changes and now is a string instead of a number, we need to first change the type in MyType, then change the implementation in fn, then resolve the issues in doubleCount. With my approach you only change the implementation, and then get errors were is relevant (like doubleCount). No need to update types.

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.

If you want your function to return one or the other, and you're returning both, you'll know it as soon as you try to use it in a place where you're expecting a single type and you get both:

const fn = (arg: number) => (arg > 50 ? "100" : 1);

const double = (number: number) => number * 2;

double(fn(10)); // Error here: Argument of type 'string | number' is not assignable to parameter of type 'number'
Enter fullscreen mode Exit fullscreen mode

The difference is that now I can figure out if I want fn to return a number or if I want to handle the case where fn returns a string:

const fn = (arg: number) => (arg > 50 ? 100 : 1);

// or
const parseNumber = (numberOrString: number | string) =>
    typeof numberOrString === "string"
        ? parseInt(numberOrString, 10)
        : numberOrString;
const double = (number: number) => number * 2;

double(parseNumber(fn(10)));
Enter fullscreen mode Exit fullscreen mode

But in terms of design, types are typically formulated before the implementations and defined in type space (rather than extracted from value space).

I agree to a certain degree, but as I stated before, the types you "design first" should be there just to help out with the JS part, not for the sake of types. Generally, you need to type arguments to let others and yourself in the future know what a function takes, but the output should be defined by the implementation. You don't lose any safety and you still gain lots of flexibility.

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

With this I disagree again, I feel a closer analogy would be that types derived from implementation are like having a map of your code, that updates automatically as you change it. If a mountain comes out of nowhere, you'll know. From my point of view, the usefulness of TypeScript comes when you augment JavaScript with it, not when you have to code in JavaScript to accommodate TypeScript types.

I understand that for you is valuable to make a "design first, and implement after", but I prefer to use types to help me out while I implement. More often than not, when you start coding the implementation you'll figure out that you might need changes in the design. With my approach, I just make those changes and types let me know if that change had any negative effect, but with yours, you need to go back to the types first, resolve the change there, and just then work on the implementation.

Just to clarify, is not just a whim or anything like that. I originally started using TypeScript because it had classes when JS didn't, and I was able to write code in a similar manner to C++ but on the web, so I get where you're coming from. Nowadays, after years of working with this language, not only did I stop using classes altogether, but also after working in lots of projects and companies with lots of people I found that the best use for TS in WebDevs is the one I suggest:

  • Is easier to grasp for people not familiar with TS.
  • Is easier to maintain and update.
  • Is equally safe, if you avoid any, non-null assertion and all those "I'm smarter than the language" kinds of things.
  • Is faster because we focus on the implementation, and the types are derived from that.
Thread Thread
 
peerreynders profile image
peerreynders • Edited on

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
 
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?

Collapse
 
lukeshiru profile image
Luke Shiru

Without annotations you'll also get it, in every place you try to use it expecting a string ☺️

Thread Thread
 
snigo profile image
Igor Snitkin

What about this place ;)

<div>{numberToString(42)}</div>
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
lukeshiru profile image
Luke Shiru

If you had that typo, you're passing a function to a ReactNode, which can be a function, so you'll not get errors there. Kinda weird example being that you can actually do this, if let's say your 42 is in a value constant:

<div>{value}</div>
Enter fullscreen mode Exit fullscreen mode

And if you want to show it next to another string you can just:

<div>The value is {value}</div>
Enter fullscreen mode Exit fullscreen mode

But I guess you wanted to make a point that in places were you're expecting Function | string, if you pass the output of that function with a typo, then you'll not spot the error. Following this logic, you actually need to type absolutely everything, because even if you don't have that numberToString util, you could make the same mistake inline:

const number = 42;
const value = number.toString; // oops, no error!

return <div>{value}</div>;
Enter fullscreen mode Exit fullscreen mode

So because you tend to do mistakes in your code, lets just not use inference at all, and type everything!

const number: number = 42;
const value: string = number.toString; // error!

return <div>{value}</div>;
Enter fullscreen mode Exit fullscreen mode

I love types, but I really don't see the point in using TypeScript like that. You should be able to figure out the mistake quite quickly using types only where you need them and using inference everywhere else, in the example above, if you don't type value, the inferred type is (radix?: number | undefined) => string, and you can see that by just hovering over it in any descent IDE. With an approach like the last example, you're just making the barrier of entry to TS higher to folks coming from JS just for the sake of it. My position is:

Use types when you need them

This also applies to return types, if you can't trust what you'll return, then type it. I'll argue that if you don't trust what you'll return the issue is elsewhere, but still. The barrier of entry is way lower because you're effectively just using JavaScript with types when needed. And the code ends up being way cleaner because is JavaScript with a few types sprinkled on top.

The other position is:

Use types always, specially in return types.

This makes the barrier of entry way higher for no good reason other than "I like to design first", which I mentioned already feels like how folks solved stuff with classes, or that "waterfall" kind of coding that never worked. We generally design as we code, and code as we design ... is an iterative and incremental activity, and as such doesn't work to "design first".

Both approaches have type for the return, but mine only has it if we actually need it to make our code more readable, not always.

Thread Thread
 
snigo profile image
Igor Snitkin • Edited on

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

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

Thread Thread
 
lukeshiru profile image
Luke Shiru

That's more an issue with ReactNode that whit the way we are doing stuff. ReactNode is one step away from being any, the type definition TL;DR is something like this:

type ReactNode =
    | (() => ReactNode)
    | {}
    | boolean
    | null
    | number
    | ReactNode[]
    | string
    | undefined;
Enter fullscreen mode Exit fullscreen mode

So even if you had made sure that numberToString is returning string, a ReactNode will not help you catch any other error. That's because a ReactNode generally is what goes inside {} in JSX, so that's basically "any expression". That's why I provided an example without React because the same applies if you do something like this:

const message = (value: any) => `Value is: ${value}`;

message(numberToString(42)); // no errors!
Enter fullscreen mode Exit fullscreen mode

You avoid this with better practices:

const message = (value: string) => `Value is: ${value}`;

message(numberToString(42)); // Error!
Enter fullscreen mode Exit fullscreen mode

But long story short is that types won't give you any safety with ReactNode, even if you type the output of your functions to avoid mistakes there, if you don't do proper validation/testing of what you put in your JSX, you'll end up getting errors either way. One way of dealing with this is for example in custom components where you want your content to be string, you don't use ReactNode for that. So we could have an util type like:

export type TypedChildren<
    Type = unknown,
    Tag extends keyof JSX.IntrinsicElements = "div",
> = Omit<JSX.IntrinsicElements[Tag], "children"> & { children?: Type };
Enter fullscreen mode Exit fullscreen mode

And then we create a "safe" element from that:

import type { VFC } from "react";
import type { TypedChildren } from "~/types/TypedChildren";

export const StringDiv: VFC<TypedChildren<string>> = props => (
    <div {...props} />
);
Enter fullscreen mode Exit fullscreen mode

And then you just:

<StringDiv>hello world</StringDiv>; // ok!
<StringDiv>{42}</StringDiv>; // Error, children has to be string
<StringDiv>{numberToString(42)}</StringDiv>; // Error again if you made that typo
Enter fullscreen mode Exit fullscreen mode
Collapse
 
dailydevtips1 profile image
Chris Bongers Author

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.

Collapse
 
lukeshiru profile image
Luke Shiru

Sorry we went all out with the debate πŸ˜… ... my initial point is that we should be making super clear to people interested in TS that you don't have to type everything, just what you need. This is mainly to lower the barrier of entry. One of my favorite introductions to TS is when we show that plain ol' JavaScript is valid TS, and then we add some types to make it more useful.

Thread Thread
 
dailydevtips1 profile image
Chris Bongers Author

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.

Collapse
 
lukeshiru profile image
Luke Shiru

Part of the context? What do you mean by that?

Thread Thread
 
siy profile image
Sergiy Yevtushenko

When you write code, you have a local context - business logic, algorithm, variables/parameters and their types. If you need to call some function, there is a narrower local context too - what you need to pass as parameters and what you'll get back. If you only writing code, often you need to take a look at function declaration to figure out what needs to be passed and what it will return. If return type is inferred, you have to look inside the function code to figure out the return type. The same happens when one reads the code (especially outside the IDE). Need to look inside the code to figure out something is a clear sign that part of context is lost. And lost context is a source of distraction and increased mental overhead. Brevity is good as long as it doesn't loss context.

Thread Thread
 
lukeshiru profile image
Luke Shiru

If return type is inferred, you have to look inside the function code to figure out the return type.

Not quite, check it here ... just hover over the add function and you'll see const add: (value1: number, value2: number) => number. Same will happen when you try to use said function, and when you import it from elsewhere. You don't have to see the implementation to get the return type.

Thread Thread
 
siy profile image
Sergiy Yevtushenko

You, seems, read my answer not carefully enough. There is often a need to read code not only in IDE.

Thread Thread
 
lukeshiru profile image
Luke Shiru

GitHub and other GIT host tools like that are making use of IntelliSense like experiences in the browser itself, soo enough you'll get that same experience in the browser while doing reviews as well. Unless you were talking about checking code in plain text editors ... if that's the case, IDK what to tell you.

Thread Thread
 
siy profile image
Sergiy Yevtushenko

I see no point to argue. I prefer to be explicit, preserve context and use language capabilities to express my intents. I believe that this is important, because my projects usually living many years and often it's not me who reads and changes my code and I can't make any assumptions in what conditions it will happen.
You prefer to rely on external, not essential and not always available tools and assume that you or someone, who will be reading your code, will have the same or similar non-essential tools. I see no problems with that. Your project, your code, your rules. I'm just trying to point that conditions could be different and your approach is not always applicable.

Thread Thread
 
lukeshiru profile image
Luke Shiru

You prefer to rely on external, not essential and not always available tools and assume that you or someone, who will be reading your code, will have the same or similar non-essential tools.

I disagree on tools like VSCode not being essential. Is almost like saying that a Jack is not essential when changing a flat tire, because you could lift the car. I mean you could, but that doesn't make the tool less essential, it just makes your workflow counterproductive. Someone coding without a proper editor/IDE is just making they own work harder without any good reason, it doesn't make sense to code thinking about people without IDEs. Under that line of thought, you should record an audio version of you reading your code in case folks don't have screens or screen readers πŸ˜…

Thread Thread
 
siy profile image
Sergiy Yevtushenko

It's sad that you consider valid only your taste. Worse is that you telling me what I should do according to your inability to accept approaches different from yours. Just keep in mind that not everyone using your favourite IDE.

Thread Thread
 
lukeshiru profile image
Luke Shiru • Edited on

It's sad that you consider valid only your taste

Is not about taste or my favorite IDE. Any IDE/Editor nowadays has this features. Are you coding without and IDE/Editor? Are you using notepad to code? Maybe is a language barrier thing and you aren't understanding my point, but anyone that codes does it with an IDE or Editor that has this kind of features beaked in. Nobody in their right mind would code with an editor that doesn't provide syntax highlight, auto-completion and so on, unless they want to intentionally make their work more complicated.

Thread Thread
 
siy profile image
Sergiy Yevtushenko

You're still missing the point: there are essential tools and there are convenience tools. Version control, build tools and compiler, etc. are essential, you can't build software without them. IDE is a convenience tool. One can change such tools without affecting ability to build software. You prefer VSCode, somebody else prefers vim or some other IDE. It's not your decision what is productive to them.
Again, I see no point to argue and don't understand why you're trying to convince me that your taste is better than others.

Thread Thread
 
lukeshiru profile image
Luke Shiru • Edited on

So as I suspected, you didn't understood, and you insist in the "taste" thing. You said it yourself:

You prefer VSCode, somebody else prefers vim or some other IDE

VIM has syntax highlight and autocompletion, like any other descent IDE. I never said: You should use VSCode, that's all you. What I said is that an editor/IDE is as essential as version control (I would argue even more, because some folks still code using FTP and no version control at all), and any modern editor/IDE has the tools that enable the coding style I have. Unless you code in a plain text editor like notepad, you'll get the same benefits one way or the other. And I say this out of experience, because at work I maintain private libraries that are used by several teams, and every dev on those teams has different editors (WebStorm, VIM, Atom, Sublime Text, etc) and my style never stopped any of them.

Again, I see no point to argue and don't understand why you're trying to convince me that your taste is better than others.

Stop trying to build that strawman out of "my taste", when I never said that. What I said is that anyone that codes for living uses an editor/IDE, and if you use that you get the benefits of coding without typing the return of functions.

I'm not trying to convince you of anything, you do you. I'm arguing what you said that we need a return type to "keep the context", when in reality any descent code editor or IDE provides that context without the need of typing the return.

Thread Thread
 
siy profile image
Sergiy Yevtushenko

VIM has syntax highlight and autocompletion, like any other descent IDE.
Not every IDE and text editor have type hinting. Not every vim setup has even syntax highlighting and autocompletion, for example.

What I said is that an editor/IDE is as essential as version control
Which is not correct. Even FTP example provided by you is a (poor) variant of version control. CI builds your code without using editor/IDE. Same can do you from command line.

Stop trying to build that strawman out of "my taste", when I never said that.
Your opinion about essentiality of editor/IDE is not technically justified, so this is just your preference or just taste.

What I said is that anyone that codes for living uses an editor/IDE, and if you use that you get the benefits of coding without typing the return of functions.
And this is not correct for two reasons - editor/IDE not always available and not every editor/IDE has type hinting.

I'm arguing what you said that we need a return type to "keep the context", when in reality any descent code editor or IDE provides that context without the need of typing the return.
I already told you, that this is your project, your code, your rules. I'm not arguing with you. But for some reason you still arguing with me and trying to convince that your point of view (based on your taste - see above) is better than mine (technically justified and based on decades of experience). I really hope that you'll never by in situation when your economy of few typed characters will not bite you hard. That said I see no point continuing discussion. Good luck.

Thread Thread
 
lukeshiru profile image
Luke Shiru

Oh ... you're one of those "I have decades if experience so you can't argue with me" kind of guy .... this is easy, Sergiy:

  • I said you can omit the return type.
  • You said "No, because you loose context".
  • I explained that in any modern editor that context is not lost, because the editor gives that to you.
  • You changed the argument to "but not everyone uses an editor/IDE, and IDEs aren't as essential as version control", and then added that I'm forcing my opinion about IDEs.
  • I said that you're doing a "straw man" because you're changing the discussion to a point that is not the one being discussed, and in top of that you're saying that an editor/IDE is not essential which doesn't make sense, mainly because is like saying: "But if I print the code, then I don't have the type there" or something ridiculous like that.

And to top it all out, you insist with the "opinion about IDEs" thing, when the discussion wasn't about that ... you made it about that when you noticed that my point about every IDE/editor out there having tooling for types was sound.

I never said that anything that I do or think is "better than yours", that's all you, mate. Sorry, but I really don't want to keep wasting time with this discussion that is going nowhere, and the fact that you played the "years of experience" card just made it even worse.

Bye!

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.

🌚 Browsing with dark mode makes you a better developer by a factor of exactly 40.

It's a scientific fact.