-
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
227: Call Signatures and Construct Signatures
A lot of developers I mentor hit a wall when they realize they can't describe a function's behavior using just the standard arrow syntax (arg: string) => void. The most common misconception I see is the belief that function type aliases and call signatures are exactly the same thing. You'll often hear, "Why do I need this weird interface syntax when I can just use a type alias with an arrow?"
Here is why that thinking fails: arrow types are great for simple functions, but they are completely incapable of describing function overloads. If you try to define a type that can be called in two different ways using only arrow syntax, you'll find yourself fighting the compiler or resorting to messy union types that lose type safety at the call site.
Arrow Types Are Enough For Every Function Definition
// The "Wrong" way: Trying to handle multiple input types with a union
type Logger = (message: string | number) => void;
const log: Logger = (msg) => {
// Inside here, 'msg' is string | number.
// I have to manually narrow the type every single time.
if (typeof msg === 'string') {
console.log("Text: " + msg);
} else {
console.log("Number: " + msg);
}
};
This works for the implementation, but it's not "overloading." It's just a function that accepts a union. If you want the type system to know that calling it with a string returns a string and calling it with a number returns a number, the arrow syntax simply cannot do it. It's too limited.
Using Call Signatures for Overloads and Flexibility
This is where Call Signatures come in. By defining the function inside an interface or a type literal without giving it a name, you can list multiple signatures for the same function. I think of this as providing a "menu" of ways the function can be invoked.
interface SmartLogger {
// These are Call Signatures
(message: string): void;
(message: number, precision: number): void;
}
const logger: SmartLogger = (msg: any, prec?: any) => {
if (typeof msg === 'string') {
console.log(`Logging string: ${msg}`);
} else {
console.log(`Logging number: ${msg} with precision ${prec}`);
}
};
// TypeScript now knows exactly which overload is being used
logger("Hello"); // Valid
logger(123.456, 2); // Valid
// logger(123.456); // Error: No overload expects 1 argument when the first is a number
Notice that I didn't give the function a name like log(): void. That's the key. If I had given it a name, it would be a method on an object. By leaving it nameless, the object itself becomes the function.
Distinguishing Between Calling and Constructing
Now, let's push this further. Sometimes you aren't just dealing with a function, but a class—or something that acts like a class. This is where Construct Signatures come in. You'll recognize these because they use the new keyword.
I've seen people try to use a call signature to describe a class constructor, but that will fail the moment you try to use the new keyword. A call signature is for fn(); a construct signature is for new Fn().
interface Point {
x: number;
y: number;
}
// This interface describes a "Constructor" function
interface PointConstructor {
new (x: number, y: number): Point;
}
function createPoint(Ctor: PointConstructor, x: number, y: number) {
return new Ctor(x, y);
}
class MyPoint implements Point {
constructor(public x: number, public y: number) {}
}
const p = createPoint(MyPoint, 10, 20);
In the example above, PointConstructor isn't an instance of a point; it's the blueprint for creating one. If you removed the new keyword, TypeScript would expect Ctor to be a regular function you call without new, and the createPoint function would throw a type error.
Personally, I find construct signatures incredibly useful when building factories or dependency injection containers where you need to pass a class around as a value but still ensure that whoever uses it can actually instantiate it correctly.
📋 Practical Task
Implementing a Typed Component Factory
You are building a UI library where components are created via a factory. You need to define a type that ensures the factory can only accept classes that follow a specific structure.
Your Goal:
- Create an interface
Componentthat has a methodrender(): string. - Create an interface
ComponentConstructorthat uses a construct signature to ensure any class passed to it returns aComponentwhen instantiated with aname: stringargument. - Implement a class
Buttonthat satisfies theComponentinterface. - Write a function
createComponentthat takes aComponentConstructorand a string, and returns a new instance of that component.
// Your code here:
interface Component {
// ...
}
interface ComponentConstructor {
// ...
}
class Button implements Component {
// ...
}
function createComponent(Ctor: ComponentConstructor, name: string) {
// ...
}
// Test it
const myButton = createComponent(Button, "Submit");
console.log(myButton.render());
There are no comments for now.