TypeScript Type Narrowing Utilities
In TypeScript, type narrowing is a powerful technique that allows the compiler to infer more specific types within certain code blocks. This leads to safer and more expressive code. This challenge asks you to create a set of utility functions that leverage and enhance type narrowing capabilities.
Problem Description
Your task is to implement several generic utility functions in TypeScript that help with type narrowing. These functions should allow developers to elegantly check for and extract specific types from union types or mixed data structures.
Key Requirements:
-
isType<T>(value: any): value is T:- This function should take a value and a type parameter
T. - It should return
trueif thevalueis of typeTandfalseotherwise. - Crucially, it must use a type predicate (
value is T) so that TypeScript can narrow down the type ofvaluewithin conditional blocks where this function returnstrue. - This is a foundational utility, so consider how to make it as general and robust as possible for primitive types and potentially object shapes.
- This function should take a value and a type parameter
-
assertType<T>(value: any, errorMessage?: string): asserts value is T:- Similar to
isType, but instead of returningfalse, it should throw an error if thevalueis not of typeT. - It should accept an optional
errorMessagestring. If not provided, a default error message should be used. - This function must also use a type predicate (
asserts value is T) for type narrowing.
- Similar to
-
filterByType<T>(values: any[]): T[]:- This function should take an array of mixed types (
any[]). - It should return a new array containing only the elements from the input array that are of type
T. - This function should internally leverage the
isTypeutility to perform the filtering and type checking.
- This function should take an array of mixed types (
Expected Behavior:
- The utilities should correctly identify primitive types (string, number, boolean, null, undefined, symbol, bigint).
- They should also be capable of narrowing down to object types, though the definition of "object type" might be constrained by the nature of runtime JavaScript checks.
- The type predicates should enable compile-time type safety.
Edge Cases to Consider:
nullandundefined: How are these handled by type checks?- Arrays: How do you check if something is an array?
- Objects: How do you check for specific object structures (beyond just
typeof obj === 'object')? For this challenge, focus on basictypeofchecks and array checks forisTypeandassertType.filterByTypeshould work with these. - Passing
undefinedasTtoisTypeorassertType.
Examples
Example 1:
// Using isType
let data: string | number = "hello";
if (isType<string>(data)) {
// data is narrowed to string here
console.log(data.toUpperCase()); // OK
}
// Using assertType
function processValue(val: string | number) {
assertType<string>(val, "Expected a string");
// val is narrowed to string here
console.log(val.length); // OK
}
try {
processValue(123);
} catch (e: any) {
console.error(e.message); // Output: Expected a string
}
Explanation:
isType<string>(data) returns true, allowing TypeScript to treat data as string within the if block. assertType<string>(val, "Expected a string") checks if val is a string; since it's a number, it throws an error with the specified message.
Example 2:
// Using filterByType
const mixedArray: (string | number | boolean)[] = [
"apple",
123,
true,
"banana",
456,
false,
];
const stringsOnly: string[] = filterByType<string>(mixedArray);
console.log(stringsOnly); // Output: [ 'apple', 'banana' ]
const numbersOnly: number[] = filterByType<number>(mixedArray);
console.log(numbersOnly); // Output: [ 123, 456 ]
Explanation:
filterByType<string> iterates through mixedArray, uses an internal type check (presumably isType) to identify strings, and returns a new array containing only those strings. Similarly for filterByType<number>.
Example 3:
// Edge case: null and undefined
let maybeNull: string | null = null;
let maybeUndefined: number | undefined = undefined;
if (isType<string>(maybeNull)) {
console.log("This won't be printed");
} else {
console.log("maybeNull is not a string"); // Output: maybeNull is not a string
}
try {
assertType<number>(maybeUndefined, "Value should be a number");
} catch (e: any) {
console.error(e.message); // Output: Value should be a number
}
// Edge case: Array
const myArray: unknown[] = [1, "two", [3, 4]];
const arrOnly: number[][] = filterByType<number[]>(myArray); // This might not work as expected with basic typeof checks
console.log(arrOnly); // Expected: [] (if basic checks are used)
Explanation:
isType<string>(null) correctly returns false. assertType<number>(undefined) throws an error. The array example highlights the limitations of basic runtime checks if deep structural typing is required; for this challenge, focus on Array.isArray for arrays and typeof for primitives.
Constraints
- Your solution must be written entirely in TypeScript.
- The primary goal is to demonstrate correct type predicate usage and generic utility function design. Performance is not a critical concern for this challenge.
- The
isTypeandassertTypefunctions should primarily rely on JavaScript's built-in runtime type checking mechanisms (typeof,instanceof,Array.isArray). Avoid complex runtime reflection libraries. - The
filterByTypefunction should be generic enough to work with any typeTthat can be checked byisType.
Notes
- Remember that JavaScript's
typeof nullreturns"object". You'll need to handle this specific case. - Consider how you will check for arrays.
Array.isArray()is the standard JavaScript way. - For
isType<T>andassertType<T>, the implementation will largely depend on how you map TypeScript types to JavaScript runtime checks. For example,Tbeingstringmight map totypeof value === 'string'. - Think about the return types of your functions, especially the use of
value is Tandasserts value is T. This is where the magic of type narrowing happens!