Narrowing Flashcards

1
Q

What is narrowing?

A

The process of refining types to more specific types than declared is called narrowing.

TypeScript follows possible paths of execution that our programs can take to analyze the most specific possible type of a value at a given position. It looks at these special checks (called type guards) and assignments.

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

typeof type guards

A

In TypeScript, checking against the value returned by typeof is a type guard. Because TypeScript encodes how typeof operates on different values.

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

Explain truthiness narrowing ?

A

In JavaScript, we can use any expression in conditionals, &&s, ||s, if statements, Boolean negations (!), and more. Constructs like if first “coerce” their conditions to booleans to make sense of them, and then choose their branches depending on whether the result is true or false.

Values like 0, NaN, "" (the empty string), 0n (the bigint version of zero), null, undefined all coerce to false, and other values get coerced true. You can always coerce values to booleans by running them through the Boolean function, or by using the shorter double-Boolean negation (!!). (The latter has the advantage that TypeScript infers a narrow literal boolean type true, while inferring the first as type boolean.)

// both of these result in 'true'
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true,    value: true
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

Explain equality narrowing

A

TypeScript uses switch statements and equality checks like ===, !==, ==, and != to narrow types.

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

Explain in operator narrowing

A

JavaScript has an operator for determining if an object has a property with a name, the in operator. TypeScript takes this into account as a way to narrow down potential types.

For example:

type Fish = { swim: () => void };
type Bird = { fly: () => void };
 
function move(animal: Fish | Bird) {
  if ("swim" in animal) {  // (parameter) animal: Fish
    return animal.swim();
  }
 
  return animal.fly();  // (parameter) animal: Bird
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

Explain instanceof
narrowing

A

JavaScript has an operator for checking whether or not a value is an “instance” of another value. More specifically, in JavaScript x instanceof Foo checks whether the prototype chain of x contains Foo.prototype.

TypeScript uses instanceof as a type guard, and narrows types in branches guarded by instanceofs.

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

Explain assignment narrowing

A

TypeScript looks at the right side of the assignment and narrows the left side appropriately.

let x = Math.random() < 0.5 ? 10 : "hello world!"; // let x: string | number
   
x = 1;
 
console.log(x); // let x: number
           
x = "goodbye!";
 
console.log(x); // let x: string
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q

Explain control flow analysis

A

TypeScript not only narrows looking for type guards in ifs, whiles, conditionals, etc. It also does an analysis of code based on reachability called control flow analysis.

TypeScript uses this flow analysis to narrow types as it encounters type guards and assignments. When a variable is analyzed, control flow can split off and re-merge over and over again, and that variable can be observed to have a different type at each point. For example:

function padLeft(padding: number | string, input: string) {
  if (typeof padding === "number") {
    return " ".repeat(padding) + input;
  }
  return padding + input;
}

padLeft returns from within its first if block. TypeScript was able to analyze this code and see that the rest of the body (return padding + input;) is unreachable in the case where padding is a number. As a result, it was able to remove number from the type of padding (narrowing from string | number to string) for the rest of the function.

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

What is a type predicate?

A

A type predicate is a user-defined type guard which takes the form of parameterName is Type, where parameterName must be the name of a parameter from the current function signature:

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

pet is Fish is our type predicate in this example. Any time isFish is called with some variable, TypeScript will narrow that variable to that specific type if the original type is compatible.

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

Explain discriminated unions

A

A discriminated union is a union of two or more interface that share a common discrimated property. For example instead of:

interface Shape {
  kind: "circle" | "square";
  radius?: number;
  sideLength?: number;
}

You should consider creating a discriminated function like the following:

interface Circle {
  kind: "circle";
  radius: number;
}
 
interface Square {
  kind: "square";
  sideLength: number;
}
 
type Shape = Circle | Square;

In the example kind is the property used to discriminate against the different possible objects. If you filter by kind === 'circle' you won’t need to do a type assertion as to use the radious as you would have needed with the initial interface.

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

Explain the never type

A

When narrowing, you can reduce the options of a union to a point where you have removed all possibilities and have nothing left. In those cases, TypeScript will use a never type to represent a state which shouldn’t exist.

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

Explain exhaustiveness checking

A

The never type is assignable to every type; however, no type is assignable to never (except never itself). This means you can use narrowing and rely on never turning up to do exhaustive checking in a switch statement.

For example, adding a default to our getArea function which tries to assign the shape to never will raise when every possible case has not been handled.

type Shape = Circle | Square;
 
function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

Adding a new member to the Shape union, will cause a TypeScript error:

interface Triangle {
  kind: "triangle";
  sideLength: number;
}
 
type Shape = Circle | Square | Triangle;
 
function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      const _exhaustiveCheck: never = shape; // Error: Type 'Triangle' is not assignable to type 'never'.
      return _exhaustiveCheck;
  }
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly