loading...

The Power of Flexible Types in TypeScript

saltyshiomix profile image Shiono Yoshihide ・3 min read

There are so many advantages of TypeScript than JavaScript.

In this article, I will introduce my love-to-use TypeScript use cases.

Index

  • Union Types
  • any Type
  • Overloads are Awesome

Union Types

Flexible String Union Types

// accept all strings
const name: string = 'anyone';

// stricted to string "john"
const john: 'john' = 'someone'; // type error

// union type (only "john" and "michael" are accepted)
const men: 'john' | 'michael' = 'smith'; // type error

Flexible Union Interfaces

interface OptionA {
  value: 'A';
  funcOnlyOptionA: () => void;
}

interface OptionOthers {
  value: Omit<string, 'A'>;
  funcOptionOthers: () => void;
}

Omit<T, U> is a TypeScript built-in type and it excludes U from T.

// no error
const option1: OptionA | OptionOthers = {
  value: 'A',
  funcOnlyOptionA: () => console.log('Hello Interface A'),
};

// no error
const option2: OptionA | OptionOthers = {
  value: 'B',
  funcOptionOthers: () => console.log('Hello String (without "A")'),
};

// NO ERROR!
// because this is a `union` type
const option3: OptionA | OptionOthers = {
  value: 'A',
  funcOptionOthers: () => console.log('Hello String (without "A")'),
};

But, how to compare option1 and option2?

In other words, how to get type errors in the case of option3?

Let's get into deeper:

const isOptionA = (option: any): option is OptionA => {
  return (option as OptionA).value === 'A';
};

const option: OptionA | OptionOthers = generateOption();

if (isOptionA(option)) {
  // now `option` is casted to `OptionA` automatically

  // good
  option.funcOptionA();

  // type error!
  option.funcOptionOthers();
}

any Type

Naturally, Types are Checked Automatically

interface User {
  name: string;
}

const user: User = { name: 'John' };

const updatedUser: User = {
  ...user,
  name: 'New John', // no errors
  bio: 'I am something!', // type error!
};

// but if we use `any` type, we can't get the benefit above:

const person = { name: 'Alex' }; // `person` is `any` type

const updatedPerson = {
  ...person,
  name: 'AlexxX', // no errors
  bio: 'I am a hacker', // no errors!
};

any Type can Convert ANY Types

See a simple example here:

interface User {
  name: string;
}

const possiblyUserObject: any = {};

const typedUser: User = possiblyUserObject; // no errors

console.log(typedUser.name); // undefined

It seems natural enough and there is nothing to study. lol

Anything else? Sure.

We can Convert JSON Object to the User Defined Types

Consider that we will fetch user data from a server. We may define the http client like this way:

(by using a http library named ky)

import ky from 'ky-universal';

const prefixUrl: string = 'http://localhost';

export class Http {
  public async get(url: string) {
    return ky.get(url, { prefixUrl }).json();
  }

  public async post(url: string, data?: any) {
    return ky.post(url, { prefixUrl, json: data }).json();
  }

  public async put(url: string, data?: any) {
    return ky.put(url, { prefixUrl, json: data }).json();
  }

  public async delete(url: string) {
    return ky.delete(url, { prefixUrl }).json();
  }
}

And we can use this Http class like below:

const http = new Http();

const user = await http.get('api/user/1');

console.log(user.name); // no errors, because `user` is `any` type

Yes it works perfectly, but we can't get benefit from TypeScript.

So let's rewrite Http class with TypeScript generics:

import ky from 'ky-universal';

const prefixUrl: string = 'http://localhost';

export class Http {
  // because json is `any` type, so we can cast it to the generic type `T`
  public async get<T>(url: string): Promise<T> {
    return ky.get(url, { prefixUrl }).json();
  }

  public async post<T>(url: string, data?: any): Promise<T> {
    return ky.post(url, { prefixUrl, json: data }).json();
  }

  public async put<T>(url: string, data?: any): Promise<T> {
    return ky.put(url, { prefixUrl, json: data }).json();
  }

  public async delete<T>(url: string): Promise<T> {
    return ky.delete(url, { prefixUrl }).json();
  }
}

And finally we can get intellisense (code complete, auto correct and so on) from your IDE like editors such as VSCode:

const http = new Http();

const user = await http.get<User>('api/user/1');
// or:
// const user: User = await http.get('api/user/1'); // no errors!

console.log(user.name); // this user is the type `User`!

Overloads are Awesome

JavaScript can't create overloads as far as I know.

If we fetch user data by id or ids, the source code is something like this:

// JavaScript

function fetchData(id) {
  if (Array.isArray(id)) {
    return [
      { name: 'john' },
      { name: 'alex' },
    ];
  }
  return { name: 'john' };
}

const data1 = fetchData(1);
console.log(data1.name); // john

const data2 = fetchData([1, 2]);
console.log(data2[0].name); // john

// but, if `id` is object, we must check the return data is array or not:
const data3 = fetchData(idOrIds);
if (Array.isArray(data3)) {
  // data3 is users
} else {
  // data3 is user
}

On the other hand, it is possible to create overloads in TypeScript:

// define overloads!
function fetchData(id: number): User
function fetchData(ids: number[]): User[]

// implementation!
function fetchData(id: number | number[]): User | User[] {
  if (Array.isArray(id)) {
    return [
      { name: 'john' },
      { name: 'alex' },
    ];
  } else {
    return { name: 'john' };
  }
}

const data1 = fetchData(1);
console.log(data1.name); // john (no errors! awesome!)

const data2 = fetchData([1, 2]);
console.log(data2[0].name); // john (no errors! awesome!)

Posted on by:

Discussion

markdown guide
 

I could not understand the overload example.
I'm confused that there is only one implementation of "fetchData". Also, what is the need to define the function twice? Can't we just write the implementation part and call that function with different types of data?