Functions Flashcards

1
Q

What is a function type expression?

A

A function type expression is the simplest way to describe a function. These types are syntactically similar to arrow functions.

The syntax (a: string) => void means “a function with one parameter, named a, of type string, that doesn’t have a return value”. Just like with function declarations, if a parameter type isn’t specified, it’s implicitly any.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

What are call signatures?

A

In JavaScript, functions can have properties in addition to being callable. However, the function type expression syntax doesn’t allow for declaring properties. If we want to describe something callable with properties, we can write a call signature in an object type:

type DescribableFunction = {
  description: string;
  (someArg: number): boolean;
};

function doSomething(fn: DescribableFunction) {
  console.log(fn.description + " returned " + fn(6));
}

Note that the syntax is slightly different compared to a function type expression - use : between the parameter list and the return type rather than =>.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

What are construct signatures?

A

JavaScript functions can also be invoked with the new operator. TypeScript refers to these as constructors because they usually create a new object. You can write a construct signature by adding the new keyword in front of a call signature:

type SomeConstructor = {
  new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
  return new ctor("hello");
}

Some objects, like JavaScript’s Date object, can be called with or without new. You can combine call and construct signatures in the same type arbitrarily:

interface CallOrConstruct {
  new (s: string): Date;
  (n?: number): number;
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

What are generic functions?

A

In TypeScript, generics are used when we want to describe a correspondence between two values. We do this by declaring a type parameter in the function signature:

function firstElement<Type>(arr: Type[]): Type | undefined {
  return arr[0];
}

TypeScript will always try to infer the generic types for their usage in functions. For example:

function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
  return arr.map(func);
}
 
// Parameter 'n' is of type 'string'
// 'parsed' is of type 'number[]'
const parsed = map(["1", "2", "3"], (n) => parseInt(n));
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

How can you constrain the generic types of a functions?

A

With extends.

Sometimes we want to relate two values, but can only operate on a certain subset of values. In this case, we can use a constraint to limit the kinds of types that a type parameter can accept.

Fore example:

function longest<Type extends { length: number }>(a: Type, b: Type) {
  if (a.length >= b.length) {
    return a;
  } else {
    return b;
  }
}
 
// longerArray is of type 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString is of type 'alice' | 'bob'
const longerString = longest("alice", "bob"); 
// Error! Numbers don't have a 'length' property
const notOK = longest(10, 100);
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

Common errors when constraining generics.

A

Returning the constrain object instead of the infered type.

Example:

function minimumLength<Type extends { length: number }>(
  obj: Type,
  minimum: number
): Type {
  if (obj.length >= minimum) {
    return obj;
  } else {
    return { length: minimum };
// Error Type '{ length: number; }' is not assignable to type 'Type'.
  }
}

It might look like this function is OK - Type is constrained to { length: number }, and the function either returns Type or a value matching that constraint. The problem is that the function promises to return the same kind of object as was passed in, not just some object matching the constraint.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q

How can you manually specify the generic type in a function

A

Adding the Type between <> before the argument list when calling the generic functions.

function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
  return arr1.concat(arr2);
}

const arr = combine<string | number>([1, 2, 3], ["hello"]);
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q

When should you specify the generic type in a generic function?

A

Whenever Typescript can’t infer the type properly.
For Example:

function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
  return arr1.concat(arr2);
}

Normally it would be an error to call this function with mismatched arrays:

const arr = combine([1, 2, 3], ["hello"]);
// Error Type 'string' is not assignable to type 'number'.

If you intended to do this, however, you could manually specify Type:

const arr = combine<string | number>([1, 2, 3], ["hello"]);
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
9
Q

How can you add optional parameters to a function type expression?

A

Marking the parameter as optional with ?:

function f(x?: number) {
  // ...
}
f(); // OK
f(10); // OK
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
10
Q

How can you provide a parameter default to a function?

A

With = like in JavaScript.

function f(x = 10) { /* ... */ }
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
11
Q

What are function overloads and how can you create them in TypeScript?

A

In TypeScript, we can specify a function that can be called in different ways by writing overload signatures. To do this, write some number of function signatures (usually two or more), followed by the body of the function:

function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
  if (d !== undefined && y !== undefined) {
    return new Date(y, mOrTimestamp, d);
  } else {
    return new Date(mOrTimestamp);
  }
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3); // Error: No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.

In this example, we wrote two overloads: one accepting one argument, and another accepting three arguments. These first two signatures are called the overload signatures.

Then, we wrote a function implementation with a compatible signature. Functions have an implementation signature, but this signature can’t be called directly. Even though we wrote a function with two optional parameters after the required one, it can’t be called with two parameters!

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
12
Q

What is the problem with this code:

function fn(x: string): void;
function fn() {
  // ...
}

fn();
A

The signature of the implementation is not visible from the outside. When writing an overloaded function, you should always have two or more signatures above the implementation of the function.

The implementation signature must also be compatible with the overload signatures.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
13
Q

What is the problem with this code:

function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {
  return x.length;
}
A

This function is fine; we can invoke it with strings or arrays. However, we can’t invoke it with a value that might be a string or an array, because TypeScript can only resolve a function call to a single overload:

len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]);
// ERROR: No overload matches this call . . . 

Because both overloads have the same argument count and same return type, we can instead write a non-overloaded version of the function:

function len(x: any[] | string) {
  return x.length;
}

Always prefer parameters with union types instead of overloads when possible

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
14
Q

Explain control flow infered this

A

TypeScript will infer what the this should be in a function via code flow analysis, for example in the following:

const user = {
  id: 123,
 
  admin: false,
  becomeAdmin: function () {
    this.admin = true;
  },
};

TypeScript understands that the function user.becomeAdmin has a corresponding this which is the outer object user.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
15
Q

How can you declare this in a function?

A

The JavaScript specification states that you cannot have a parameter called this, and so TypeScript uses that syntax space to let you declare the type for this in the function body.

interface DB {
  filterUsers(filter: (this: User) => boolean): User[];
}
 
const db = getDB();
const admins = db.filterUsers(function (this: User) {
  return this.admin;
});

This pattern is common with callback-style APIs, where another object typically controls when your function is called. Note that you need to use function and not arrow functions to get this behavior:

interface DB {
  filterUsers(filter: (this: User) => boolean): User[];
}
 
const db = getDB();
const admins = db.filterUsers(() => this.admin); // Error: The containing arrow function captures the global value of 'this'. Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
16
Q

What does the void type represents?

A

void represents the return value of functions which don’t return a value. It’s the inferred type any time a function doesn’t have any return statements, or doesn’t return any explicit value from those return statements:

// The inferred return type is void
function noop() {
  return;
}
17
Q

What does the object type refers to?

A

The special type object refers to any value that isn’t a primitive (string, number, bigint, boolean, symbol, null, or undefined). This is different from the empty object type { }, and also different from the global type Object. It’s very likely you will never use Object.

object is not Object. Always use object!

Note: that in JavaScript, function values are objects: They have properties, have Object.prototype in their prototype chain, are instanceof Object, you can call Object.keys on them, and so on. For this reason, function types are considered to be objects in TypeScript.

18
Q

What does the unknown type represents?

A

The unknown type represents any value. This is similar to the any type, but is safer because it’s not legal to do anything with an unknown value.

This is useful when describing function types because you can describe functions that accept any value without having any values in your function body.

Conversely, you can describe a function that returns a value of unknown type:

function safeParse(s: string): unknown {
  return JSON.parse(s);
}
 
// Need to be careful with 'obj'!
const obj = safeParse(someRandomString);
19
Q

What does the never type represents?

A

Some functions never return a value:

function fail(msg: string): never {
  throw new Error(msg);
}

The never type represents values which are never observed. In a return type, this means that the function throws an exception or terminates execution of the program.

never also appears when TypeScript determines there’s nothing left in a union.

function fn(x: string | number) {
  if (typeof x === "string") {
    // do something
  } else if (typeof x === "number") {
    // do something else
  } else {
    x; // has type 'never'!
  }
}
20
Q

What does the global type Function describes?

A

The global type Function describes properties like bind, call, apply, and others present on all function values in JavaScript. It also has the special property that values of type Function can always be called; these calls return any:

function doSomething(f: Function) {
  return f(1, 2, 3);
}

This is an untyped function call and is generally best avoided because of the unsafe any return type.

If you need to accept an arbitrary function but don’t intend to call it, the type () => void is generally safer.

21
Q

What are rest parameters?

A

In JavaScript (and in TypeScript) we can define functions that take an unbounded number of arguments using rest parameters.

A rest parameter appears after all other parameters, and uses the ... syntax:

function multiply(n: number, ...m: number[]) {
  return m.map((x) => n * x);
}
// 'a' gets value [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4);

In TypeScript, the type annotation on these parameters is implicitly any[] instead of any, and any type annotation given must be of the form Array<T>or T[], or a tuple type.

22
Q

What are rest arguments?

A

We can provide a variable number of arguments from an array using the spread syntax. For example, the push method of arrays takes any number of arguments:

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(...arr2);

TypeScript Handbook: Rest Parameters. (n.d.). Retrieved March 23, 2023, from https://www.typescriptlang.org/docs/handbook/2/functions.html#rest-arguments

23
Q

What’s the problem with spread arguments in Typescript?

A

In general, TypeScript does not assume that arrays are immutable. This can lead to some surprising behavior:

// Inferred type is number[] -- "an array with zero or more numbers",
// not specifically two numbers
const args = [8, 5];
const angle = Math.atan2(...args);
// Error: A spread argument must either have a tuple type or be passed to a rest parameter.

The best fix for this situation depends a bit on your code, but in general a const context is the most straightforward solution:

// Inferred as 2-length tuple
const args = [8, 5] as const;
// OK
const angle = Math.atan2(...args);

TypeScript Handbook: Rest Parameters. (n.d.). Retrieved March 23, 2023, from https://www.typescriptlang.org/docs/handbook/2/functions.html#rest-arguments

24
Q

What is parameter destructuring?

A

It is just the good old destructuring assignment in JavaScript.
The type annotation for the object goes after the destructuring syntax:

function sum({ a, b, c }: { a: number; b: number; c: number }) {
  console.log(a + b + c);
}

This can look a bit verbose, but you can use a named type here as well:

// Same as prior example
type ABC = { a: number; b: number; c: number };
function sum({ a, b, c }: ABC) {
  console.log(a + b + c);
}

TypeScript Handbook: Parameter Destructuring. (n.d.). Retrieved March 23, 2023, from https://www.typescriptlang.org/docs/handbook/2/functions.html#parameter-destructuring

25
Q

What is different about the assignability of functions that return void type?

A

The void return type for functions can produce some unusual, but expected behavior.

Contextual typing with a return type of void does not force functions to not return something. Another way to say this is a contextual function type with a void return type (type vf = () => void), when implemented, can return any other value, but it will be ignored.

Thus, the following implementations of the type () => void are valid:

type voidFunc = () => void;
 
const f1: voidFunc = () => {
  return true;
};
 
const f2: voidFunc = () => true;
 
const f3: voidFunc = function () {
  return true;
};

And when the return value of one of these functions is assigned to another variable, it will retain the type of void:

const v1 = f1();
const v2 = f2();
const v3 = f3();

This behavior exists so that the following code is valid even though Array.prototype.push returns a number and the Array.prototype.forEach method expects a function with a return type of void.

const src = [1, 2, 3];
const dst = [0];
 
src.forEach((el) => dst.push(el));

There is one other special case to be aware of, when a literal function definition has a void return type, that function must not return anything.

function f2(): void {
  // @ts-expect-error
  return true;
}
 
const f3 = function (): void {
  // @ts-expect-error
  return true;
};

TypeScript Handbook: Void Return Type. (n.d.). Retrieved March 23, 2023, from https://www.typescriptlang.org/docs/handbook/2/functions.html#return-type-void