TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
69: Type Testing with tsd or expect-type
Imagine you're a precision machinist. You're carving a bolt out of titanium for a high-performance engine. It's not enough for the bolt to "look like a bolt" or even "fit roughly" into the hole. If it's off by 0.01 millimeters, the whole engine could seize at 7,000 RPM. You don't wait until the engine is running to find out if the bolt is the right size; you use a micrometer to measure the part in isolation. If the measurement is wrong, you scrap the part before it ever touches the engine.
In TypeScript, your types are the blueprints, and your logic is the engine. Usually, we rely on the compiler to tell us when something is wrong while we're writing code. But when you're building a library or a complex set of generic utilities, you need a "micrometer" for your types. You need a way to assert that a specific function always returns exactly string | number and never accidentally slips into string | number | undefined. That's where expect-type and tsd come in.
Why run tests on things that don't execute?
I've seen plenty of developers shrug and say, "If the code compiles, the types are correct." That's usually true for app code, but it's a dangerous assumption for utility libraries. If you change a internal type alias in a complex generic, the compiler might still be "happy" because the types are compatible, but you might have accidentally widened a type, losing the very type safety you promised your users.
Type testing allows you to write "assertions" that the TypeScript compiler checks. If the type of a variable doesn't match what you expected, the test fails during the build process—long before the code ever hits a browser or a server.
Putting your types under the microscope with expect-type
expect-type is the lightweight choice. It's essentially a set of helper functions that do nothing at runtime but trigger a compiler error if the types don't match perfectly. Let's say we have a utility that wraps a value in a standard API response envelope.
import { expectType, expectError } from 'expect-type';
interface ApiResponse<T> {
data: T;
status: number;
}
function wrapResponse<T>(data: T): ApiResponse<T> {
return { data, status: 200 };
}
// This is our "micrometer" measurement.
// If wrapResponse ever stops returning ApiResponse<string>, this line will throw a TS error.
expectType<ApiResponse<string>>(wrapResponse('Hello World'));
// We can also test that something SHOULD fail.
// This ensures that we aren't accidentally allowing numbers where strings should be.
expectError(wrapResponse<string>(123));
Notice how expectType doesn't actually "run" in the traditional sense of a Jest test. It creates a type-level constraint. If you're using a tool like tsd, it will scan these files and report any TypeScript errors as test failures.
Scaling up to tsd for library authors
While expect-type is great for quick checks, tsd is more of a full-fledged testing framework for types. It allows you to create .test-d.ts files. The magic of tsd is that it can check for "exact" type matches, which is crucial when you're dealing with complex unions or mapped types.
I typically reach for tsd when I'm publishing a package to NPM. It lets me document exactly how the types should behave for the end-user. For example, if I have a type that converts a User object into a UserDTO, I can write a test that proves the password field is definitely stripped out of the resulting type, ensuring no developer can accidentally leak sensitive data just because a type was loosened.
The workflow is simple: you write your type assertions in a .test-d.ts file, run the tsd command, and it uses the TypeScript compiler to verify that every assertion holds true. It's a bit of overhead, sure, but it's the only way to sleep soundly knowing your generics aren't lying to you.
📋 Practical Task
Validating the UserRole Extractor
You are building a permissions system. You have a utility type called ExtractRole that should take a complex User object and return only the role string. However, you need to ensure that it correctly handles cases where the role might be missing or nested.
Your Task:
- Create a type
Userthat has an optionalprofileobject, which in turn has arole(either 'admin', 'editor', or 'viewer'). - Implement a utility type
ExtractRole<T>that extracts that role string. - Using
expect-type(assume it is installed), write three type assertions:- Assert that a user with an 'admin' role returns exactly
'admin'. - Assert that a user with no profile returns
undefined. - Use
expectErrorto assert that passing a plain string into yourExtractRoleutility (instead of a User object) causes a type error.
- Assert that a user with an 'admin' role returns exactly
Note: Focus on the type-level assertions. You don't need to write the runtime logic for the extraction, just the type definitions and the tests.
There are no comments for now.