Categories
TypeScript

Introduction to TypeScript Enums

We look at how to define and use enums in TypeScript.

We look at how to define and use enums in TypeScript

If we want to define constants in JavaScript, we can use the const keyword. With TypeScript, we have another way to define a set of constants call the enums. Enums let us define a list of named constants. It’s handy for defining an entity that can take on a few possible values. TypeScript provides both numeric and string-based enums.

Numeric Enums

TypeScript has an enum type that’s not available in JavaScript. An enum type is a data type that has a set named values called elements, members, enumeral or enumerator of the type. They’re identifiers that act like constants in the language. In TypeScript, a numeric enum has a corresponding index associated with it. The members start with the index 0 by default, but it can be changed to start at any index we like and the subsequent members will have indexes that increment from that starting number instead. For example, we can write the following code to define a simple enum in TypeScript:

enum Fruit { Orange, Apple, Grape };

We can use enums by accessing the members like any other property. For example, in the Fruit enum, we can accept the members like in the following code:

console.log(Fruit.Orange);
console.log(Fruit.Apple);
console.log(Fruit.Grape);

Then console.log from the code above should get us 0 since we didn’t specify a starting index for the enum. We can specify the starting index of an enum with something like in the following code:

enum Fruit { Orange = 1, Apple, Grape };
console.log(Fruit.Orange);
console.log(Fruit.Apple);
console.log(Fruit.Grape);

Then we get the following logged from each console.log statement in order:

1
2
3

We can specify the same index for each member, but it wouldn’t be very useful:

enum Fruit { Orange = 1, Apple = 1, Grape };
console.log(Fruit.Orange);
console.log(Fruit.Apple);
console.log(Fruit.Grape);

Then we get:

1
1
2

from the console.log . As we can see, we specify the index pretty much however we want to change it. We can even have negative indexes:

enum Fruit { Orange = -1, Apple, Grape };
console.log(Fruit.Orange);
console.log(Fruit.Apple);
console.log(Fruit.Grape);

Then we get:

-1
0
1

from the console.log . To get an enum member by its index, we can just use the bracket notation like we access array entries by its index. For example, we can write the following code:

enum Fruit { Orange, Apple, Grape };
console.log(Fruit[0]);
console.log(Fruit[1]);
console.log(Fruit[2]);

Then we get:

Orange
Apple
Grape

Numeric enums can have computed values assigned to their members. For example, we can write a function to get a value for each enum member like in the following code:

const getValue = () => 2;

enum Fruit {
  Orange = getValue(),
  Apple = getValue(),
  Grape = getValue()
};

Note that we assigned a return value for each member. If we don’t do that for all of them like in the following code:

const getValue = () => 2;

enum Fruit {
  Orange = getValue(),
  Apple = getValue(),
  Grape
};

Then the TypeScript compiler won’t compile the code and will give an error “Enum member must have initializer.(1061)“. We can mix both constant and computed values in one enum, so we can write something like:

const getValue = () => 2;

enum Fruit {
  Orange = getValue(),
  Apple = 3,
  Grape = getValue()
};

String Enums

TypeScript enum members can also have string values. We can set the values of each member to a string by assigning strings to them like in the following code:

enum Fruit {
  Orange = 'Orange',
  Apple = 'Apple',
  Grape = 'Grape'
};

However, unlike numeric enums, we can’t assign computed values to them. For example, if we have the following:

const getValue = () => 'Orange';

enum Fruit {
  Orange = getValue(),
  Apple = 'Apple',
  Grape = 'Grape'
};

Then we would get the TypeScript compiler error message “Computed values are not permitted in an enum with string-valued members. (2553)” since computed values aren’t allowed for string-valued enums. String enums don’t have auto-incrementing behavior like numeric enums since they don’t have numerical values, but the values of the enum members are much clearer since each value is a meaningful value that’s clear to any human reading it.

In a single enum, we can have some members having numeric values and others having string values like in the following code:

enum Fruit {
  Orange = 2,
  Apple = 'Apple',
  Grape = 'Grape'
};

However, this is more confusing than having a single type of value for all enum members, so it’s not recommended to have mixed value types for different members for an enum.

Computed and Constant Members

Each enum member has a value associated with it which can either be constant or computed. An enum member is constant if the first member in the enum has no value explicitly assigned to it, which means that it’s assigned the value 0 by default. It can also be considered constant if it doesn’t have an explicit value assigned to it and the preceding enum member was a numeric constant, which means that it’ll have the value of the preceding member plus one. For example, if we have:

enum Fruit { Orange = 1, Apple, Grape };

Then Apple and Grape are both constant members since it’ll be automatically assigned the values 2 and 3 respectively. They’re also considered constant if each member has string values assigned to them. Also, if an enum references a previously defined enum member which can be from the same or a different enum. The return value of any operation assigned to constant enums like surrounding an enum expression with parentheses, doing unary arithmetic or bitwise operations to an enum expression like +, -, ~ , or doing binary arithmetic or bitwise operations like, -, *, /, %, <<, >>, >>>, &, |, ^ with enum expressions as operands are all considered constant enum expressions.

For example, the following enum is an enum with constant enum expressions:

enum Fruit {
  Orange = 1 + 2,
  Apple =  1 + 3,
  Grape = 1 + 4
};

The expressions are constant since they’re computed from any variable or return values of functions. Each member has values that’s computed from number values, rather than numbers assigned to variables or returned from functions.

The following is also an example of an enum with constant members:

enum Fruit {
  Orange = 1 + 2,
  Apple =  1 + 3,
  Grape = Orange + Apple
};

All the members including the last one are constant since the value of Grape is computed from Orange and Apple which are constant. Bitwise operations with both operands being constant values are also considered constants as we have in the following code:

enum Fruit {
  Orange = 1 | 2,
  Apple =  1 + 3,
  Grape = 'abc'.length
};

Anything else not described above is considered computed values. For example, if we have:

enum Fruit {
  Orange = 1 + 2,
  Apple =  1 + 3,
  Grape = 'abc'.length
};

Then Grape is a computed member since the expression we assigned to Grape is not computed from any constant member, and it involves getting a property from an object, which isn’t computed from a constant value.

If we want to define constants in JavaScript, we can use the const keyword. With TypeScript, we have another way to define a set of constants call the enums. Enums let us define a list of named constants. It’s handy for defining an entity that can take on a few possible values. TypeScript provides both numeric and string-based enums. TypeScript allows enum members to have numeric and string values. They can also be computed from values for other enum members or from any other expression we wish to assign. Constant enums are the ones that are computed from actual numerical values as operands or with actual values assigned to the member. All other values are computed member values.

Categories
TypeScript

Cool New Features Released with TypeScript 3.6

Lots of new features are released with TypeScript 3.6. It includes features for iterables like stricter type checking for generators, more accurate array spreading, allow get and set in declare statements, and more.

In this article, we’ll look at each of them.

Stricter Type Check for Generators

With TypeScript 3.6, the TypeScript compiler has more checks for data types in generators.

Now we have a way to differentiate whether our code yield or return from a generator.

For example, if we have the following generator:

function* bar() {
    yield 1;
    yield 2;
    return "Finished!"
}

let iterator = bar();
let curr = iterator.next();
curr = iterator.next();

if (curr.done) {
    curr.value
}

The TypeScript 3.6 compiler knows automatically that curr.value is a string since we returned a string at the end of the function.

Also, yield isn’t assumed to be of any type when we assign yield to something.

For instance, we have the following code:

function* bar() {
    let x: { foo(): void } = yield;
}

let iterator = bar();
iterator.next();
iterator.next(123);

Now the TypeScript compiler knows that the 123 isn’t assignable to something with the type { foo(): void } , which is the type of x . Whereas in earlier versions, the compiler doesn’t check the type of the code above.

So in TypeScript 3.6 or later, we get the error:

Argument of type '[123]' is not assignable to parameter of type '[] | [{ foo(): void; }]'.

Type '[123]' is not assignable to type '[{ foo(): void; }]'.

Type '123' is not assignable to type '{ foo(): void; }'.

Also, now the type definitions for Generator and Iterator have the return and throw methods present and iterable.

TypeScript 3.6 converts the IteratorResult to the IteratorYieldResult<T> | IteratorReturnResult<TReturn> union type.

It can also infer the value that’s returned from next() from where it’s called.

For example, the following would compile and run since we passed in the right type of value into next() , which is a string:

function* bar() {
    let x: string = yield;
    console.log(x.toUpperCase());
}

let x = bar();
x.next();
x.next('foo');

However, the following would fail to compile because of type mismatch between the argument and the type of x :

function* bar() {
    let x: string = yield;
    console.log(x.toUpperCase());
}

let x = bar();
x.next();
x.next(42);

We would have to pass in a string to fix the that arises from x.next(42); . Also, the TypeScript compiler knows that the first call to next() does nothing.

More Accurate Array Spread

With TypeScript 3.6, the transformation of some array spread operators now produce equivalent results when the code is transpiler to ES5 or earlier targets with the --downlevelIteration on.

The flag’s purpose is to transform ES6 iteration constructs like the spread operator and for...of loop to add support for ES6 iteration constructs into code that’s transpiled to something older than ES6.

For example, if we have:

[...Array(3)]

We should get:

[undefined, undefined, undefined]

However, TypeScript versions earlier than 3.6 changes […Array(3)] to Array(3).slice();

Which gets us an empty array with length property set to 3.

Raise Error with Bad Promise Code

TypeScript 3.6 compiler will let us know if we forget to put await before promises in async functions or forget to call then after promises.

For example, if we have:

interface Person {
    name: string;
}

let promise: Promise<Person> = Promise.resolve(<Person>{ name: 'Joe' });
(async () => {
    const person: Person = promise;
})();

Then we get the error:

Property 'name' is missing in type 'Promise<Person>' but required in type 'Person'.

Putting in await before promise would fix the problem:

interface Person {
    name: string;
}

let promise: Promise<Person> = Promise.resolve(<Person>{ name: 'Joe' });
(async () => {
    const person: Person = await promise;
})();

Something writing something like:

(async () => {
      fetch("https://reddit.com/r/javascript.json")
        .json()
})();

will get us the error:

Property 'json' does not exist on type 'Promise<Response>'.(2339)

input.ts(3, 10): Did you forget to use 'await'?

The following will fix the error:

(async () => {
  const response = await fetch("[https://reddit.com/r/javascript.json](https://reddit.com/r/javascript.json)")
  const responseJson = response.json()
})();

Unicode Character Identifiers

Now we can use Unicode characters for identifiers with TypeScript. For example:

const ? = 'foo';

would work with TypeScript 3.6 compiler or later.

get and set Accessors Are Allowed in Declare Statements

We can add get and set to declare statements now. For example, we can write:

declare class Bar {
  get y(): number;
  set y(val: number);
}

The generated type definitions will also emit get and set accessors in TypeScript 3.7 or later.

Merging Class and Constructor Function Declare Statements

With TypeScript 3.6 or later, the compiler is smart enough to merge function constructors and class declare statements with the same name. For example, we can write:

export declare function Person(name: string, age: number): Person;
export declare class Person {
    name: string;
    age: number;
    constructor(name: string, age: number);
}

It knows that the function is a constructor and the class is the same as the function.

The signatures of the constructors in the function and class constructor don’t have to match, so the following:

export declare function Person(name: string): Person;
export declare class Person {
    name: string;
    age: number;
    constructor(name: string, age: number);
}

still works.

Semicolons

Now TypeScript is smart enough to add semicolons automatically to places that requires it by style conventions instead of adding it automatically to every statement.

TypeScript 3.6 is another feature-packed release. It focuses on improving features like inferring types and type checks in generators, more accurate array spread for code emitted in ES5 or earlier.

Also, bad promise code will raise errors, like the ones that missed await or then .

Merging function constructor and class code in declare statements are also supported now.

Unicode characters are now supported in identifiers, and semicolons won’t be added automatically on every line.

Categories
TypeScript

How does TypeScript Know Which Types are Compatible with Each Other? — Enums and More

The advantage of TypeScript over JavaScript is that we can annotate data types of our variables, functions, and other entities with it. This lets us get auto-completion in our text editor and it also adds a compile step to building apps which means that compile-time checks can catch more errors like undefined error and unexpected data type errors that would otherwise be caught at runtime.

To do this, TypeScript checks if different variables have the same type according to the structure and other aspects of an entity, like the number of parameters and the type of each parameter and so on. In this article, we’ll continue to look at how TypeScript determines which data types are compatible with each other.

Determining Compatible Function Parameter Types

When passing in values into functions as arguments, the parameter assignment will succeed if either the source parameter is assignable to the target parameter or vice versa. This involves having actions that can’t be determined at compile-time but it’s still allowed. This means that we can pass in arguments that are of a more specialized type than the one specified by a parameter to maintain compatibility with JavaScript. For example, we can do something like in the following code:

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

interface Employee {
  name: string;
  age: number;
  employeeCode: string;
}

const fn = (person: Person) => person;
const employee: Employee = {
  name: 'Jane',
  age: 20,
  employeeCode: '123'
}

fn(employee);

In the code above, we passed in a variable of the type Employee , which has more properties than the person parameter, which has the type Person . This means that, like in JavaScript, we can choose to ignore properties of objects that are passed into functions. Of course, the employeeCode property can’t be accessed inside the fn function, so if we write:

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

interface Employee {
  name: string;
  age: number;
  employeeCode: string;
}

const fn = (person: Person) => console.log(person.employeeCode);
const employee: Employee = {
  name: 'Jane',
  age: 20,
  employeeCode: '123'
}

fn(employee);123'
}

fn(employee);

We would get the error message:

Property 'employeeCode' does not exist on type 'Person'.(2339)

To disallow this behavior, we can enable the strictFunctionTypes flag when we compile the code.

Optional Parameters and Rest Parameters

In TypeScript, optional and required parameters are interchangeable for the sake of function parameter type compatibility checks. Extra parameters of the source type isn’t an error, optional parameters of the target type without corresponding parameters of the source type is also not an error. For example, the following code is allowed:

const runLater = (callbackFn: (...args: any[]) => void) => 0;
runLater((a, b) => { });
runLater((a, b, c) => { });

We can pass in any function as a callback function since we used the rest operation to specify that we can pass in any number of arguments in the callback function that we pass to the runLater function. Functions with rest parameters are treated as if it has an infinite number of parameters. This is OK since not passing in anything to a function if the same as passing in undefined in JavaScript.

Overloaded Functions

Functions overloads, where there’re multiple signatures for a function with the same name, must have arguments that match at least one of the signatures for TypeScript to accept the function call as valid. For example, if we have the following function overload:

function fn(a: { b: number, c: string, d: boolean }): number
function fn(a: { b: number, c: string }): number
function fn(a: any): number {
  return a.b;
};

Then we have to call the fn function by passing in at a number for the first argument and a string for the second argument like we do below:

fn({ b: 1, c: 'a' });

TypeScript will check against each signature to see if the property structure of the object we pass in as the argument matches any of the overloads. If it doesn’t match like if we have the following:

fn({ b: 1 });

Then we get the following error message:

No overload matches this call.

Overload 1 of 2, '(a: { b: number; c: string; d: boolean; }): number', gave the following error.

Argument of type '{ b: number; }' is not assignable to parameter of type '{ b: number; c: string; d: boolean; }'.

Type '{ b: number; }' is missing the following properties from type '{ b: number; c: string; d: boolean; }': c, d

Overload 2 of 2, '(a: { b: number; c: string; }): number', gave the following error.

Argument of type '{ b: number; }' is not assignable to parameter of type '{ b: number; c: string; }'.

Property 'c' is missing in type '{ b: number; }' but required in type '{ b: number; c: string; }'.(2769)
input.ts(2, 29): 'c' is declared here.

Enums

In TypeScript, enums are considered to be compatible with numbers and vice versa. However, enum values from different enum types are considered incompatible. For example, if we have:

enum Fruit { Orange, Apple };
enum Bird { Chicken, Duck, Goose };

let fruit = Fruit.Orange;
fruit = Bird.Chicken;

Then in the last line, we’ll get the error message:

Type 'Bird.Chicken' is not assignable to type 'Fruit'.(2322)

Classes

Type compatibility of classes works like object literal types. However, only instance members of each class are checked for compatibility. Static members are ignored for type compatibility checks. For example, if we have:

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  static getNumPersons() {
    return 1;
  }
}

let animal: Animal = new Animal('Jane');
let person: Person = new Person('Joe');
animal = person;

Then the code would be accepted by the TypeScript compiler since animal and person are considered to be variables with compatible types. Static members like constructor and the getNumPersons method is ignored when the TypeScript compiler checks for type compatibility.

Private and protected members are checked during type compatibility checks when they’re included in a class. For example, if we have the following code:

class Animal {
  name: string;
  private age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  static getNumPersons() {
    return 1;
  }
}

let animal: Animal = new Animal('Jane');
let person: Person = new Person('Joe');
animal = person;

Then the TypeScript will reject the code with the following error:

Property 'age' is missing in type 'Person' but required in type 'Animal'.(2741)

input.ts(3, 11): 'age' is declared here.

TypeScript will only accept the code if the private member of both classes are derived from the same source. For example, if we have:

class Animal {
  private age: number = 20;
}

class Dog extends Animal {
  name: string;
  constructor(name: string) {
    super();
    this.name = name;
  }
}

class Cat extends Animal{
  name: string;
  constructor(name: string) {
    super();
    this.name = name;
  }
}

let dog: Dog = new Dog('Jane');
let cat: Cat = new Cat('Joe');
dog = cat;

Then the TypeScript compiler will accept the code when we try to compile it since the private member age is in the Animal class which the Dog and Cat classes are sub-classes of. This also to apply to protected members.

Generics

Type compatibility checks for generics are also derived from their structure. For example, if we have:

interface Person<T> {
  name: string;
}
let x: Person<number> = { name: 'Joe' };
let y: Person<string> = { name: 'Jane' };
x = y;

Then they’re compatible since both x and y are of type Person and we didn’t reference the generic type marker in any member of the interface. However, if we do reference a member in the Person interface, as we do in the following code:

interface Person<T> {
  name: string;
  age: T
}
let x: Person<number> = { name: 'Joe', age: 10 };
let y: Person<string> = { name: 'Jane', age: '20' };
x = y;

Then the type we pass into the <> does matter since we’re using it to determine the type of age . If we try to compile the code above, we would get the error:

Type 'Person<string>' is not assignable to type 'Person<number>'.

Type 'string' is not assignable to type 'number'.(2322)

For them to be considered to be compatible types, the type we pass in have to be the same in both variable declarations as we have below:

interface Person<T> {
  name: string;
  age: T
}

let x: Person<number> = { name: 'Joe', age: 10 };
let y: Person<number> = { name: 'Jane', age: 20 };
x = y;

In TypeScript code, we can pass in arguments that are of a more specialized type than the one specified by a parameter to maintain compatibility with JavaScript. For functions with overloads, then the arguments that we pass in have to match the parameter types that are declared in the function overloads. For classes, only instance members are considered to when determining if 2 classes are of compatible types. This applies to public members. For private and protected members, they have to be derived from the same super-class for 2 classes to be determined to have compatible types. Enums are compatible with numbers and vice versa, but enum values from 2 different enums aren’t compatible with each other. Generics are considered compatible with each by following the same rules as objects, functions, and classes. The types that are passed into a generic will be considered like any other code for type compatibility.

Categories
TypeScript

How does TypeScript Know Which Types are Compatible with Each Other? — Objects and Functions

The advantage of TypeScript over JavaScript is that we can annotate data types of our variables, functions, and other entities with it. This lets us get auto-completion in our text editor and it also adds a compile step to building apps which means that compile-time checks can catch more errors like undefined error and unexpected data type errors that would otherwise be caught at runtime.

To do this, TypeScript checks if different variables have the same type according to the structure and other aspects of an entity, like the number of parameters and the type of each parameter and so on. In this article, we’ll look at how TypeScript determines which data types are compatible with each other.

TypeScript’s type system allows some operations that can’t be known at compile-time to be safe. A type system is sound when it doesn’t allow actions that aren’t known at compile time. However, since TypeScript is a superset of JavaScript, it has to allow some unsound actions to be done at compile-time.

Basic Type Compatibility

The most basic rule that TypeScript used to determine type compatibility is that if both types have the same structure, then it’s considered to be compatible types. Having the same structure means that both types have the same member names and each member has the same type. For example, if we have:

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

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

let x: A = { name: 'Joe', age: 10 };
let y: B = { name: 'Jane', age: 20 };
x = y;

Then TypeScript would accept the code and compile it since both interfaces A and B both have name and age as their fields and both name and age in both interfaces have the same type. Also, if an object has all the properties of a type and extra properties that aren’t in the interface, then the object can also be assigned to a variable without a type annotation, which can then be assigned to a variable with a set type. For instance, if we have the following code:

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

let y: A;
let x = { name: 'Joe', age: 10, gender: 'male' };
y = x;

Where y has the data type A , then we can assign an object to a variable with the without a data type annotation, which is x , and then we can assign to y which has the type A . This is because only object literals have the excess property check. Variables do not have this check.

Likewise, data types of functions are also checked when assigning a function to a variable. We can pass in an object that has more properties than the type that type of the parameter. For example, we can write something like the following code:

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

let person = { name: 'Joe', age: 10, gender: 'male' };

function echo(person: Person) {
  return person;
}

echo(person);

The person object has a gender property that isn’t in the Person interface. but the person parameter in the echo function is of type Person . Given that, we can still pass in person into as the first argument of the echo function call. TypeScript doesn’t care if there’re extra properties in the argument that we pass in. On the other hand, if our argument has missing properties, then TypeScript will give us an error and won’t compile the code. For instance, if we have the following code:

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

let person = { name: 'Joe' };

function echo(person: Person) {
  return person;
}

echo(person);

Then we would get the error:

Argument of type '{ name: string; }' is not assignable to parameter of type 'Person'.

Property 'age' is missing in type '{ name: string; }' but required in type 'Person'.(2345)
input.ts(3, 3): 'age' is declared here.

The structural check for type compatibility is done recursively. So the same checks apply to objects with whatever nesting level it has. For example, if we have:

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

let person = { name: 'Joe', age: 20, address: {street: '123 A St.'} };

function echo(person: Person) {
  return person;
}

echo(person);

This would pass the structural type checking done by TypeScript because all the properties at each level are present. On the other hand, if we have:

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

let person = { name: 'Joe', age: 20, address: { } };

function echo(person: Person) {
  return person;
}

echo(person);

Then the TypeScript compiler would give us the following error:

Argument of type '{ name: string; age: number; address: {}; }' is not assignable to parameter of type 'Person'.

Types of property 'address' are incompatible.

Property 'street' is missing in type '{}' but required in type '{ street: string; }'.(2345)

input.ts(5, 5): 'street' is declared here.

since the street property is missing from the object assigned to the address property.

Comparing Functions

Comparing objects and primitive values for their types are pretty straightforward. However, determining which functions have types that are compatible with each other is harder. In TypeScript, we can assign a function that has a function that has fewer parameters to one with a function that has more parameters but otherwise have the same function signature. For example, if we have:

let x = (a: number) => 0;
let y = (a: number, b: string) => 0;

Then we can assign x to y like we do in the following code:

y = x;

This is because, in JavaScript, we can often assign functions that should have more parameters by default by ones with fewer parameters. For example, the array map method takes a callback function which has 2 parameters which are the array entry that’s being processed and the array index of the entry. However, we sometimes just pass in a callback function that only has the first parameter like in the following code:

let arr = ['a', 'b', 'c'];
arr.map(a => a);

TypeScript has to allow for the discarding of parameters to maintain compatibility with JavaScript. Likewise, for comparing return types, TypeScript determines that a function with a return type that has more properties is compatible with ones with fewer properties but otherwise has the same structure. For example, if we have 2 functions like in the following code:

let fn1 = () => ({ name: "Alice" });
let fn2 = () => ({ name: "Joe", age: 20 });

Then we can assign fn2 to fn1 like in the following code:

fn1 = fn2;

However, the other way around wouldn’t work:

fn2 = fn1;

If we try to compile the code above, we would get the following error from the TypeScript compiler:

Type '() => { name: string; }' is not assignable to type '() => { name: string; age: number; }'.

Property 'age' is missing in type '{ name: string; }' but required in type '{ name: string; age: number; }'.(2322)

input.ts(2, 33): 'age' is declared here.

As we can see, TypeScript accepts a return type that has more properties as ones that have fewer properties but otherwise have the same structure.

TypeScript checks the data type of objects and functions by their structure. Generally, if 2 types have the same properties and data types for each, then they’re considered to be the same types. Otherwise, objects that have more properties than another one, but otherwise have the same structure are also considered to be compatible. Likewise, if a function has more parameters than another one with fewer parameters, but otherwise have the same structure, than they’re considered the same. This also applies to the structure of objects that are returned by functions. If one function returns an object has more properties than another function which returns an object with fewer properties than the other function, but otherwise have the same structure, then the 2 return types are considered to be compatible.

Categories
TypeScript

Introduction to TypeScript Generics — Functions

One way to create reusable code is to create code that lets us use it with different data types as we see fit. TypeScript provides the generics construct to create reusable components where we can work with variety of types with one piece of code. This allows users to use these components and by putting in their own types. In this article, we’ll look at the many ways to define a generic function where we can set the types of parameters and the return value to avoid redefining functions that have the same logic but different parameter types and return types.

Defining Generic Functions

One basic use case for generics is for creating functions that has the same logic, but have different types for parameters and their return types. For example if we want to create an identity function, where we just return the same thing that’s passed in, we may write something like:

function echo(arg: number): number {
  return arg;
}

If we want this function to take accept more than one type as a parameter and return the same type that’s passed in, then we can turn it into a generic function by putting in a generic type marker instead of putting in the number type for the arg parameter and the return type. We denote a type variable with the T keyword. The T keyword allows us to capture the group for the generic function later. We can switch out number for T like in the following code:

function echo<T>(arg: T): T {
  return arg;
}

Then when we call the function, we can write something like:

console.log(echo<number>(1));
console.log(echo<string>('string'));
console.log(echo<boolean>(true));

Then we get the following out:

1
string
true

With the code above, we get type checking in each function call for the argument passed in and the return type. The code inside the <> denotes the type that we want the parameter and the return type that the function will accept for the parameter and the return type. We don’t have to put in the type explicitly like we did above. TypeScript is smart enough to identify the type as long as we define the generic function. For example, if we write the following code to call the echo function:

console.log(echo(1));
console.log(echo('string'));
console.log(echo(true));

The code will still compile and run and output the same things as it did above. This keeps the code shorter, but explicitly setting the type is clearer for developers and compiler may sometimes fail to identify the type, especially when the code is more complex. In this case, the type must be provided.

If we want to access specific properties of objects, then we have to be more specific with our generic types. For example, if we assume that the type for the parameter and the return type are always some kind of array, then we can specify that with the following code:

function echo<T>(arg: T[]): T[] {
  console.log(arg.length);
  return arg;
}

Equivalently, we can write that with the Array<T> generic type instead like in the following code:

function echo<T>(arg: Array<T>): Array<T> {
  console.log(arg.length);
  return arg;
}

In both pieces of code, we can log the length property of the array that we pass into the function as our argument since it’s guaranteed that what we pass in and return are always arrays. For example, we can call the new echo function like in the following code:

echo([1, 2, 3]);
echo(['a', 'b', 'c']);

Type inference would still work in the case above.

In the above examples, we have one parameter and one return value. But what if we want to pass in multiple parameters with different types and return one or more of them? We can write something like the following:

function echo<T, U>(arg: T, arg2: U): [T, U] {
  return [arg, arg2];
}

In the code above, we have 2 generic types, T and U , which represents same or different types. For example, we can use it like in the following code:

console.log(echo(1, 'a'));

In the code above, we have the first argument being a number and the second being a string, but we can also have both being the same type like we have in the following code:

console.log(echo(1, 2));

Variation of Type Markers

Another issue that we’re going to run into eventually is that we want parameters that have data types that are different from the return types. One way to solve this is to mix generic types and regular types with interfaces like we do in the following code:

interface EchoInterface {
  [prop: string]: any
}

function echo<T, U, V, W>(a: T, b: U, c: V, d: W): EchoInterface {
  return {a,b,c,d};
}

console.log(echo(1, 'a', false, {}));

Then we get the following from the console.log :

{a: 1, b: "a", c: false, d: {}}

We can also leave out the return type like we do in the following code:

function echo<T, U, V, W, X>(a: T, b: U, c: V, d: W) {
  return { a, b, c, d };
}

console.log(echo(1, 'a', false, {}));

We get the same output as we do above. Note that in the 2 examples above, we can specify as many letters as we want as generic type markers. Generic type markers doesn’t have to be one letter. It’s just convention for it to be a single letter. We can write something like the following:

function echo<T, U, V, W, AA>(a: T, b: U, c: V, d: AA) {
  return { a, b, c, d };
}

Another way to define a generic function is to put the signature in the interface along with the generic type markers there. Then when declare the function by assigning it to a variable, we can set the type of the variable with the interface and then assign the generic function to it with the usual generic type markers added to the function. For example, we can write something like the following code:

interface EchoFn {
  <T>(a: T): T;
};

function echo<T>(a: T): T {
  return a;
}

const e: EchoFn = echo;

We may also add the generic type marker inside the <> to the interface declaration like in the following code:

interface EchoFn<T> {
  <T>(a: T): T;
};

Then when we declare the variable that we assign the function to, we have to specify the type of the interface explicitly, like in the following code:

interface EchoFn<T> {
  <T>(a: T): T;
};

function echo<T>(a: T): T {
  return a;
}

const e: EchoFn<number> = echo;

In the code above, we have to add the number type declaration explicitly in the last line instead of letting TypeScript infer it automatically.

To avoid redefining functions that have the same logic but different parameter types and return types, we can define generic functions. To do this, we add generic type markers to our functions, and in interfaces that are used for typing the functions. We define functions by inserting generic type markers inside the <> after the function name, with each marker separated by commas, and also after the colon in parameters, and after the colon and before the open curly bracket. The generic type marker for the return type is optional. We can leave it out if we want to return something that has a different type than what we pass into the parameters. Likewise, in interfaces, we put the signature of the function in the interface and optionally in the interface definition to add enforce generic type markers in the functions we define.