TypeScript Essentials: Crafting Simple Types

Implementation of Simple Types About TypeScript TypeScript is a highly expressive language that allows you to write algorithms using types alone. However, it is impossible to express every type. For example, in TypeScript, the maximum length of a tuple that can be represented by types is 999. Depending on the version of TypeScript, some restrictions may be relaxed or new features may be added. In this process, types that were previously impossible to express might become possible, but there are still types that cannot be represented. In most cases, this is not a matter of the language's expressiveness but rather a restriction to prevent using too many computations. Type Level From now on, we must strictly distinguish between JavaScript and TypeScript. Node.js developers now handle TypeScript for types and JavaScript for values. If a developer cannot create custom types, then the developer's code is not much different from plain JavaScript. Even if types are strictly defined in the code, you cannot say that TypeScript is being used to its full extent. What features are there in TypeScript that are specifically for TypeScript? Let’s discuss what distinguishes JavaScript from TypeScript. TypeScript vs. JavaScript const example1: number = 1; const example2: string = '2'; const example3: (...param: any[]) => any = () => {}; This is how you annotate identifiers with types in TypeScript. A colon is used to specify the type, and aside from that, it’s the same as JavaScript. The same applies to functions; however, the function must be annotated with a function-like type. At that time, the function can also specify parameter and return types. Here, we express that any remaining parameters are represented by the rest operator as an any[] and the return type is also any. Now, what types can be assigned to identifiers? Primitive Types and any TypeScript has the types string, number, and boolean. If you understand up to this point, you have essentially learned all the types in JavaScript. Most strongly typed languages begin programming by predefining these primitive types to variables. In TypeScript, if no type is specified, it defaults to any, so up to this point everything is the same. Therefore, since no type is specified in JavaScript, you can think of every type as any in TypeScript. Wrapper Objects and Primitive Types In JavaScript, wrapper objects such as Number, Boolean, and String exist. So you can create objects of primitive types like Number(3). However, these wrapper objects are generally discouraged in TypeScript. The typeof operator, which we use frequently, does not refer to these wrapper objects at either the value level or the type level. Literal Types Unlike most other languages, TypeScript can express types that are more specific than the primitive types. These are called literal types; for example, there can be a type of 'hello world'. While 'hello world' is a string, a string is not necessarily 'hello world'. Likewise, the number 10 is of type number, but number is not equivalent to the literal 10. type Example = 'helloWorld'; const e: Example = 'helloWorld'; // Assigning any value other than 'helloWorld' will result in an error. This declaration of literal types is one of the main reasons TypeScript can express so many different types. Nullable Types JavaScript has both undefined and null. TypeScript also has undefined and null types, and these two are strictly distinct types. If undefined or null are used as property types in an object, they are entirely separate from properties that were not specified at all. In other words, a property not specified (which evaluates to undefined in JavaScript) is different from a property explicitly set to undefined. If your IDE does not distinguish between these, be sure to check your tsconfig.json file. If you’re not familiar with tsconfig.json yet, you can ignore this. // tsconfig.json "strictNullChecks": true never Type Next, there is the never type. In JavaScript, every type is any. Therefore, in TypeScript, you could say that every type is a subset of any. So what do you call a type that contains nothing? TypeScript calls this type never, representing the empty set in type theory. never and extends in Conditional Types When a condition is applied to never, the condition never actually executes. This is similar to how methods on Array.prototype never iterate if the array is empty. Since never represents an empty set that contains nothing, there is never any type to check against the condition. type Example = T extends any ? true : false; type e = Example; // It is inferred as never, not true or false. Checking for never type IsNever = any type e = IsNever; However, you must know how to check whether a type is never or not. One way to check for never i

Mar 29, 2025 - 09:19
 0
TypeScript Essentials: Crafting Simple Types

Implementation of Simple Types

About TypeScript

TypeScript is a highly expressive language that allows you to write algorithms using types alone.

However, it is impossible to express every type.

For example, in TypeScript, the maximum length of a tuple that can be represented by types is 999.

Depending on the version of TypeScript, some restrictions may be relaxed or new features may be added.

In this process, types that were previously impossible to express might become possible, but there are still types that cannot be represented.

In most cases, this is not a matter of the language's expressiveness but rather a restriction to prevent using too many computations.

Type Level

From now on, we must strictly distinguish between JavaScript and TypeScript.

Node.js developers now handle TypeScript for types and JavaScript for values.

If a developer cannot create custom types, then the developer's code is not much different from plain JavaScript.

Even if types are strictly defined in the code, you cannot say that TypeScript is being used to its full extent.

What features are there in TypeScript that are specifically for TypeScript?

Let’s discuss what distinguishes JavaScript from TypeScript.

TypeScript vs. JavaScript

const example1: number = 1;
const example2: string = '2';
const example3: (...param: any[]) => any = () => {};

This is how you annotate identifiers with types in TypeScript.

A colon is used to specify the type, and aside from that, it’s the same as JavaScript.

The same applies to functions; however, the function must be annotated with a function-like type.

At that time, the function can also specify parameter and return types.

Here, we express that any remaining parameters are represented by the rest operator as an any[] and the return type is also any.

Now, what types can be assigned to identifiers?

Primitive Types and any

TypeScript has the types string, number, and boolean.

If you understand up to this point, you have essentially learned all the types in JavaScript.

Most strongly typed languages begin programming by predefining these primitive types to variables.

In TypeScript, if no type is specified, it defaults to any, so up to this point everything is the same.

Therefore, since no type is specified in JavaScript, you can think of every type as any in TypeScript.

Wrapper Objects and Primitive Types

In JavaScript, wrapper objects such as Number, Boolean, and String exist.

So you can create objects of primitive types like Number(3).

However, these wrapper objects are generally discouraged in TypeScript.

The typeof operator, which we use frequently, does not refer to these wrapper objects at either the value level or the type level.

Literal Types

Unlike most other languages, TypeScript can express types that are more specific than the primitive types.

These are called literal types; for example, there can be a type of 'hello world'.

While 'hello world' is a string, a string is not necessarily 'hello world'.

Likewise, the number 10 is of type number, but number is not equivalent to the literal 10.

type Example = 'helloWorld';
const e: Example = 'helloWorld'; // Assigning any value other than 'helloWorld' will result in an error.

This declaration of literal types is one of the main reasons TypeScript can express so many different types.

Nullable Types

JavaScript has both undefined and null.

TypeScript also has undefined and null types, and these two are strictly distinct types.

If undefined or null are used as property types in an object, they are entirely separate from properties that were not specified at all.

In other words, a property not specified (which evaluates to undefined in JavaScript) is different from a property explicitly set to undefined.

If your IDE does not distinguish between these, be sure to check your tsconfig.json file.

If you’re not familiar with tsconfig.json yet, you can ignore this.

// tsconfig.json
"strictNullChecks": true

never Type

Next, there is the never type.

In JavaScript, every type is any.

Therefore, in TypeScript, you could say that every type is a subset of any.

So what do you call a type that contains nothing?

TypeScript calls this type never, representing the empty set in type theory.

never and extends in Conditional Types

When a condition is applied to never, the condition never actually executes.

This is similar to how methods on Array.prototype never iterate if the array is empty.

Since never represents an empty set that contains nothing, there is never any type to check against the condition.

type Example<T> = T extends any ? true : false;

type e = Example<never>; // It is inferred as never, not true or false.

Checking for never

type IsNever<T> = any

type e = IsNever<never>;

However, you must know how to check whether a type is never or not.

One way to check for never in TypeScript is by using the following pattern:

type IsNever<T> = [T] extends [never] ? true : false;

Union Types

A union type is interpreted as “or” and is denoted by the pipe symbol.

Below, we define a type that means either a number or a string.

Since the type is defined as number or string, both 1 and '1' are acceptable values.

type Example = number | string;

const e1: Example = 1;
const e2: Example = '1';

Type Inference

If you explicitly specify a type as true | false, it is inferred as boolean since true or false is a boolean.

TypeScript refers to this phenomenon as the Best Common Type.

If there exists an expression that best represents the intended type, it will be used as a substitute.

This process of determining the most appropriate type is called type inference.

This is especially useful with union types.

type Example1 = 1 | 2 | 3 | number; // Inferred as number.
type Example2 = 'a' | 'b' | 'c' | string; // Inferred as string.
type Example3 = true | false; // Inferred as boolean.

Intersection Types

If there is an "or", then there must also be an "and".

In set theory, “or” corresponds to union and “and” corresponds to intersection.

In TypeScript, an intersection type (translated as intersection) is denoted by the ampersand (&).

That is, it represents the intersection of the defined elements.

Below, we define the intersection of a type that represents numbers 1 through 3 and a type that only allows 2.

Therefore, the type of e1 is 2, and assigning any value other than 2 will result in an error.

type Element1 = 1 | 2 | 3;
type Element2 = 2;
type Example = Element1 & Element2;

const e1: Example = 2;

Intersection in Interface Types

interface Person {
  name: string;
  age: number;
}

interface Address {
  address: string;
}

type PersonWithAddress = Person & Address; // { name: string; age: number; address: string }

In union types, an intersection picks only the common parts of the unions.

However, with interfaces, using an intersection type results in a merged type of both interfaces.

Although it may seem counterintuitive, you can understand the intersection as “the most appropriate type that satisfies both” interfaces.

For two different union types to allow both, they must share the common part of the union.

But for interfaces, the most appropriate type that satisfies both is the merged form of the two interfaces.

Array Types

There is also the Array type with a variable length and the tuple type with a fixed length, which is a bit more complex.

Tuples will be explained later.

An array represents a collection of multiple types and does not impose restrictions on the number of elements.

type Example = number[];

const e1: Example = [1,2,3,4,5];

Generic Types and Type Parameters

Functions have parameters.

Similarly, when writing types, you can define types with parameters, which allows you to write more complex types succinctly.

Types with type parameters are called generic types.

For instance, the number array type shown above can be written generically as follows:

type Example1 = number[];
type Example2 = Array<number>;

const e2: Example2 = [1,2,3,4,5];

Generic Types and Function Definitions

const example = <T extends number>(param: T): T => {
    return param;
};

The above code simply takes a parameter and returns it without any particular meaning.

However, this code helps you understand how to use generic types with functions.

The generic type is specified in angle brackets to the left of the parentheses where the parameters are passed.

Thus, T is at least a number, and the parameter param and the return type are both of type T.

Conditional Types

Types can also have conditions.

They are represented using a syntax similar to the ternary operator, with a question mark and a colon, using the extends keyword.

If you have a syntax like ‘A extends B ? C : D’, it should be interpreted as “if A extends B, then C; otherwise, D.”

type Example = 1 extends number ? true : false;

const e: Example = true; // Error if false is assigned.

Using Generics with Conditional Types

If you use a condition, naturally, you need parameters for the condition.

As in the code above, conditions like 1 extends number are usually already known.

Therefore, having a type that branches based on parameters allows for richer type expressions.

We have the type parameter T available.

type Example<T> = T extends string ? true : false;

const e: Example<'abc'> = true; // Error if false is assigned.

The above code defines a type parameter T so that the developer can provide any type, and it will be true only if T is a string.

Constraints on Generic Types

Among conditions, there are those that are used exclusively in generic types, called constraints.

A constraint should be understood more as “at least this” rather than “if ~ then”.

If you put a constraint on a generic parameter, an error will occur if the provided type does not meet the condition.

type Example<T extends string> = true;

const e1: Example<string> = true; // No problem.
const e2: Example<number> = true; // Compilation error on the generic parameter.

Conditional Types and the infer Keyword

type Example<T> = T extends (...param: any[]) => infer R ? R : never;

const add = (a: number, b: number): number => a + b;

const e1: Example<typeof add> = 3; // number

In the type above, Example will infer the return type R if T is a function type.

The infer keyword is used here (R is just an identifier with no special meaning).

If, for instance, the return type of add changes to string, then e1 must be of type string.

One thing to note is that the infer keyword infers the most minimal type that satisfies the condition.

Let’s look at another example for a more detailed explanation.

infer Keyword in String Literal Types

type Example<T extends string> = T extends `${infer F}${string}` ? F : string;

const e1: Example<"abcde"> = 'a';

In the code above, if any value other than 'a' is assigned, an error will occur.

This is because infer F is inferred as the literal type 'a'. One might wonder why it is 'a' and not 'abc'.

The answer is that TypeScript’s type inference infers the best common type, which in this case is the minimal type.

infer Keyword in Arrays

type Example<T> = T extends [infer F, ...infer Rest] ? F : never;

const e: Example<[1, 3]> = 1; // Only 1 can be assigned.

If T is a tuple consisting of F and the rest of the types (Rest), then F is inferred as the first element type.

The remaining elements are inferred as a tuple type using …infer Rest.

Thus, in the example, Example<[1, 3]> is inferred as 1.

Rest, like the identifier R in previous examples, is arbitrarily chosen and holds no special meaning.

infer Keyword with Type Parameters

type Example<T> = T extends Array<infer R> ? R : never;

const e: Example<number[]> = 3; // Since R is inferred as number, 3 can be assigned.

In conditions that are not constraints, the infer keyword can be used to express conditions.

The infer keyword is created to represent a virtual type that has not yet been determined.

While the infer keyword can work on its own, it is mostly used together with generic parameters to allow richer type expressions.

type IsNever<T> = [T] extends [never] ? true : false;

Since never cannot be checked normally, we use the tuple trick as in the IsNever type above.

as Keyword

The as keyword is used to explicitly tell the compiler the type, but if used incorrectly it can cause improper type inference.

However, there are cases where using as is unavoidable to solve a particular type problem.

const add = <T extends number>(
    a: T,
    b: number
): T extends number ? number : "error" => {
    if (typeof a !== "number") {
        return "error"; // Error: Type 'string' is not assignable to type 'T extends number ? number : "error"'.
    }
    return a + b; // Error: Type 'number' is not assignable to type 'T extends number ? number : "error"'.
};

In the above code, the add function generates errors at each return statement.

The function is intended to return number if type T is number, and the literal "error" otherwise.

Although we can understand that the internal code meets the return type requirements, the compiler does not understand the branch conditions in the if statements.

The TypeScript compiler compiles types, not the code itself.

const add = <T extends number>(
    a: T,
    b: number
): T extends number ? number : "error" => {
    if (typeof a !== "number") {
        return "error" as T extends number ? number : "error";
    }
    return (a + b) as T extends number ? number : "error";
};

Thus, we must modify the types using as.

As mentioned earlier, while the use of as should be minimized, sometimes it is necessary to explicitly specify the type.

Abbreviating When as Must Be Used

const add = <T extends number, P extends T extends number ? number : "error">(
    a: T,
    b: number
): T extends number ? number : "error" => {
    if (typeof a !== "number") {
        return "error" as P;
    }
    return (a + b) as P;
};

Here, an additional type parameter P is introduced to predefine the complex type.

Then, you only need to use as P for the repeated type annotations.

While this cleans up redundant as type assertions, it can cause issues if the developer attempts to specify P directly inside the angle brackets.

Usually, if left untouched, there is no problem.

keyof Keyword

type Example = {
  id: number;
  name: string;
};
type Keys = keyof Example; // 'id' | 'name'

The keyof keyword produces a type consisting of only the keys of the given type, inferred as a union type.

Mapped Types

type EnumType = 'a' | 'b' | 'c' | 'd';
type Example = {
  [K in EnumType]: number;
};

If you want to convert a union type back into an object type, you can use a Mapped type.

In the example above, each member of EnumType — 'a', 'b', 'c', and 'd' — is used as a key.

Thus, the inferred type is as follows:

type Example = {
  a: number;
  b: number;
  c: number;
  d: number;
};

Arrays and Mapped Types

In JavaScript, arrays are objects. How about in types?

type NumberArray = [1, 2, 3, 4, 5];
type NumberToString<T extends number> = `${T}`;

type NumberArrayToStringArray<T extends number[]> = {
    [key in keyof T]: `${T[key]}`
}

type Answer = NumberArrayToStringArray<NumberArray>; // ["1", "2", "3", "4", "5"]

Even at the type level, arrays are objects, and thus Mapped types can be applied to them.