Skip to main content

leveraging-strict-type-checking

Understand the Purpose of Strict Type Checking:

Strict type checking helps to spot possible issues by ensuring the data you use matches the types you've declared. This can prevent a lot of mistakes that could lead to bugs.

// Without strict type checking, the following code would not cause a compilation error.
let myName: string = undefined;

// With strict type checking enabled, TypeScript will report an error.

Enable Strict Mode in Configuration:

Turn on strict mode in your TypeScript configuration file to make sure that strict type checking is applied. This will help catch mistakes early in the development process.

{
"compilerOptions": {
"strict": true
}
}

Get Familiar with the unknown Type:

The unknown type is like a safe version of any. It's a way to say "this could be anything," but unlike any, you have to check what it is before you try to do anything with it.

let uncertainValue: unknown = "Could be anything";

// Doing anything with uncertainValue would be an error without checking its type first.
if (typeof uncertainValue === "string") {
console.log(uncertainValue.trim()); // Now it's safe to use string methods.
}

Use Type Guards to Narrow Types:

Type guards are a way to make sure that within a certain block of code, your variables are definitely of a certain type.

function doSomething(value: unknown) {
if (typeof value === "number") {
return value.toFixed(2); // The compiler knows value is a number here.
}
}

Implement User-Defined Type Guards:

You can create your own checks that tell TypeScript if something is a certain type by using a function that returns a special kind of boolean.

interface Bird {
fly: () => void;
}

function isBird(animal: any): animal is Bird {
return (animal as Bird).fly !== undefined;
}

Understand Type Assertions vs. Type Casting:

Type assertions are a way to tell TypeScript "I know what type this is," but it doesn't actually check at runtime. It's just for the compiler.

let someValue: unknown = "This is a string";

// Telling TypeScript to treat someValue as a string.
let strLength: number = (someValue as string).length;

Familiarize Yourself with the never Type:

The never type is used for functions that are not supposed to complete normally, like if they always throw an error or if they're supposed to run forever.

function error(message: string): never {
throw new Error(message);
}

function infiniteLoop(): never {
while (true) {
}
}

Leverage strictNullChecks:

By turning on strictNullChecks, TypeScript makes sure you don't accidentally use null or undefined where you're not supposed to, which can save you from a lot of common errors.

{
"compilerOptions": {
"strictNullChecks": true
}
}

Each of these tips ensures that TypeScript works harder to catch errors in your code, making it more reliable and easier to maintain.

Use strictFunctionTypes for Function Comparisons:

The strictFunctionTypes flag ensures that functions are compared correctly based on their parameter types. It helps in situations where you pass functions around as arguments.

{
"compilerOptions": {
"strictFunctionTypes": true
}
}

Explore strictPropertyInitialization:

This setting requires all properties to be initialized either at their declaration or in the constructor, avoiding the risk of them being undefined.

class Person {
name: string;

constructor(name: string) {
this.name = name; // With strictPropertyInitialization, this is required.
}
}

Utilize noImplicitAny:

The noImplicitAny flag warns you when the compiler defaults to the any type, prompting you to specify a more specific type.

{
"compilerOptions": {
"noImplicitAny": true
}
}

Consider noImplicitThis:

When noImplicitThis is enabled, you must define the type of this in functions that are not part of a class, to avoid it being any.

function doSomething(this: { name: string }) {
console.log(this.name);
}

Embrace strictBindCallApply:

With strictBindCallApply, TypeScript will ensure that bind, call, and apply are used correctly with the appropriate arguments.

function add(a: number, b: number) {
return a + b;
}

const numbers = [1, 2];
const sum = add.apply(null, numbers as [number, number]); // Without strictBindCallApply, this might not be checked.

Practice Using Discriminated Unions:

Discriminated unions help TypeScript understand the exact type when you have a union of multiple types, using a shared property to discriminate between them.

type Success = {
response: 'success';
data: string;
};

type Failure = {
response: 'failure';
error: Error;
};

type Result = Success | Failure;

function handleResult(result: Result) {
if (result.response === 'success') {
console.log(result.data);
} else {
console.log(result.error.message);
}
}

Understand Conditional Types for Advanced Patterns:

Conditional types let you create types that change based on the input type. They are similar to ternary operators for types.

type IsString<T> = T extends string ? 'yes' : 'no';

type A = IsString<string>; // 'yes'
type B = IsString<number>; // 'no'

These advanced TypeScript features provide a powerful toolkit for ensuring type safety and can be particularly impressive to discuss during an interview.