Object types and interfaces
Basic Concepts of Object Types and Interfaces
In TypeScript, object types and interfaces are important tools for defining data structures. Object types are used to describe the shape of a value, while interfaces are a more powerful way to define these shapes. Both can be used to constrain the structure of variables, function parameters, and return values.
// Object type
let user: { name: string; age: number } = {
name: "Alice",
age: 25
};
// Interface
interface User {
name: string;
age: number;
}
let user2: User = {
name: "Bob",
age: 30
};
Basic Usage of Interfaces
Interfaces play a key role in TypeScript. They can not only describe the structure of objects but can also be implemented by classes. Interfaces define a contract of properties and methods, and any class or object implementing the interface must satisfy these requirements.
interface Animal {
name: string;
makeSound(): void;
}
class Dog implements Animal {
name: string;
constructor(name: string) {
this.name = name;
}
makeSound() {
console.log("Woof!");
}
}
const myDog = new Dog("Buddy");
myDog.makeSound(); // Output: Woof!
Optional and Readonly Properties
Interfaces can include optional and readonly properties, providing greater flexibility in type definitions. Optional properties are marked with ?
, while readonly properties are modified with readonly
.
interface Config {
readonly id: number;
name: string;
url?: string;
}
const config1: Config = {
id: 1,
name: "App Config"
};
const config2: Config = {
id: 2,
name: "DB Config",
url: "https://example.com/db"
};
// config1.id = 3; // Error: Cannot assign to 'id' because it is a read-only property
Function Types and Indexable Types
Interfaces can describe not only objects but also function types and indexable types. Function type interfaces define the parameter list and return type of a function, while indexable type interfaces describe types that can be accessed via an index.
// Function type interface
interface SearchFunc {
(source: string, subString: string): boolean;
}
const mySearch: SearchFunc = function(src, sub) {
return src.search(sub) > -1;
};
// Indexable type interface
interface StringArray {
[index: number]: string;
}
const myArray: StringArray = ["Alice", "Bob"];
const firstItem = myArray[0]; // Type is string
Interface Inheritance and Hybrid Types
Interfaces can extend other interfaces to form more complex type definitions. TypeScript also supports hybrid types, where an interface can describe an object, function, and indexable type simultaneously.
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
const square: Square = {
color: "blue",
penWidth: 5.0,
sideLength: 10
};
// Hybrid type interface
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = function(start: number) {} as Counter;
counter.interval = 123;
counter.reset = function() {};
return counter;
}
const c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
Differences Between Type Aliases and Interfaces
Type aliases and interfaces have many similarities in functionality but also important differences. Type aliases can represent primitive types, union types, and tuples, while interfaces are better suited for defining object shapes and support extension.
// Type alias
type Point = {
x: number;
y: number;
};
// Extending an interface
interface Point3D extends Point {
z: number;
}
// Type alias can represent union types
type ID = number | string;
// Type alias can represent tuples
type Data = [number, string, boolean];
Advanced Types and Interfaces
TypeScript provides various advanced type features that can be used with interfaces, such as generic interfaces, conditional types, and mapped types. These features greatly enhance the expressiveness of the type system.
// Generic interface
interface Box<T> {
contents: T;
}
const stringBox: Box<string> = { contents: "hello" };
const numberBox: Box<number> = { contents: 42 };
// Conditional types and interfaces
interface User {
id: number;
name: string;
age: number;
}
type UserKeys = keyof User; // "id" | "name" | "age"
// Mapped types
type ReadonlyUser = {
readonly [P in keyof User]: User[P];
};
const readonlyUser: ReadonlyUser = {
id: 1,
name: "Alice",
age: 25
};
// readonlyUser.name = "Bob"; // Error: Cannot assign to 'name' because it is a read-only property
Relationship Between Interfaces and Classes
In TypeScript, interfaces and classes have a close relationship. Classes can implement one or more interfaces, ensuring they provide all members defined by the interface. This mechanism is an important way to achieve polymorphism.
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
class Clock implements ClockInterface {
currentTime: Date = new Date();
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) {}
}
// Implementing multiple interfaces
interface Alarm {
alert(): void;
}
interface Light {
lightOn(): void;
lightOff(): void;
}
class SecurityLight implements Alarm, Light {
alert() {
console.log("Alert!");
}
lightOn() {
console.log("Light on");
}
lightOff() {
console.log("Light off");
}
}
Interfaces and Function Overloading
Interfaces can be used to define function overloads, providing multiple type definitions for a function. This is particularly useful when handling different parameter combinations, offering more precise type checking.
interface OverloadedFunc {
(x: number): number;
(x: string): string;
}
const myFunc: OverloadedFunc = function(x: any): any {
if (typeof x === "number") {
return x * 2;
} else if (typeof x === "string") {
return x.toUpperCase();
}
};
const numResult = myFunc(10); // Type is number
const strResult = myFunc("hello"); // Type is string
Interfaces and Type Assertions
When TypeScript cannot automatically infer the correct type, type assertions can be used to tell the compiler the type of a value. This is particularly useful when dealing with complex objects or data from external sources.
interface Person {
name: string;
age: number;
}
const obj = {};
const person = obj as Person;
person.name = "Alice"; // Now can access properties of the Person interface
person.age = 25;
// Another assertion syntax
const person2 = <Person>{};
person2.name = "Bob";
person2.age = 30;
Interfaces and Type Guards
Type guards help us determine the type of an object at runtime. Combined with interfaces, they enable safer code and prevent type errors.
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function getSmallPet(): Fish | Bird {
// Actual implementation would return Fish or Bird
return Math.random() > 0.5 ?
{ swim: () => {}, layEggs: () => {} } :
{ fly: () => {}, layEggs: () => {} };
}
const pet = getSmallPet();
// Using type assertions
if ((pet as Fish).swim) {
(pet as Fish).swim();
} else if ((pet as Bird).fly) {
(pet as Bird).fly();
}
// Using type predicates
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
Interfaces and Generic Constraints
Generic constraints allow us to limit the type range of generic parameters. Combined with interfaces, they enable more precise generic definitions.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity("hello"); // String has length property
loggingIdentity([1, 2, 3]); // Array has length property
// loggingIdentity(3); // Error: Number does not have length property
// Using type parameters in generic constraints
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const x = { a: 1, b: 2, c: 3 };
getProperty(x, "a"); // Correct
// getProperty(x, "d"); // Error: "d" is not a property of x
Interfaces and Decorators
Although TypeScript decorators are primarily used for classes and class members, we can combine interfaces with decorators through some techniques to enhance code maintainability.
interface Serialize {
toJSON(): string;
}
function serializable<T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor implements Serialize {
toJSON() {
return JSON.stringify(this);
}
};
}
@serializable
class Person {
constructor(public name: string, public age: number) {}
}
const p = new Person("Alice", 25);
console.log(p.toJSON()); // Output: {"name":"Alice","age":25}
Interfaces and Module System
In modular development, interfaces help define contracts between modules. By exporting interfaces instead of concrete implementations, we can maintain loose coupling between modules.
// database.ts
export interface Database {
connect(): Promise<void>;
query(sql: string): Promise<any[]>;
close(): Promise<void>;
}
// postgres-db.ts
import { Database } from "./database";
export class PostgresDatabase implements Database {
async connect() {
// Implementation of connection logic
}
async query(sql: string) {
// Implementation of query logic
return [];
}
async close() {
// Implementation of close logic
}
}
// app.ts
import { Database } from "./database";
import { PostgresDatabase } from "./postgres-db";
async function runApp(db: Database) {
await db.connect();
const results = await db.query("SELECT * FROM users");
console.log(results);
await db.close();
}
runApp(new PostgresDatabase());
Interfaces and Third-Party Libraries
When using third-party JavaScript libraries, interfaces help us define types for these libraries, gaining the benefits of TypeScript's type checking. This is typically achieved through declaration files (.d.ts).
// jquery.d.ts
declare interface JQuery {
text(content?: string): string | JQuery;
on(event: string, handler: Function): JQuery;
// Other methods...
}
declare var $: {
(selector: string): JQuery;
ready(handler: Function): void;
};
// app.ts
$("#myButton").on("click", function() {
$(this).text("Clicked!");
});
$.ready(function() {
console.log("DOM is ready");
});
Interfaces and React Components
In React development, interfaces are commonly used to define the types of component Props and State, providing better type safety and code hints.
import React from 'react';
interface User {
id: number;
name: string;
email: string;
}
interface UserListProps {
users: User[];
onSelectUser: (user: User) => void;
}
const UserList: React.FC<UserListProps> = ({ users, onSelectUser }) => {
return (
<ul>
{users.map(user => (
<li key={user.id} onClick={() => onSelectUser(user)}>
{user.name} ({user.email})
</li>
))}
</ul>
);
};
// Using the component
const users: User[] = [
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" }
];
function handleSelect(user: User) {
console.log("Selected user:", user);
}
// <UserList users={users} onSelectUser={handleSelect} />
Interfaces and Vue Components
When using TypeScript with Vue 3, interfaces help define the types of component props, emits, and component instances.
import { defineComponent } from 'vue';
interface User {
id: number;
name: string;
email: string;
}
interface UserCardProps {
user: User;
isActive?: boolean;
}
export default defineComponent({
name: 'UserCard',
props: {
user: {
type: Object as () => User,
required: true
},
isActive: {
type: Boolean,
default: false
}
},
emits: ['select'],
setup(props, { emit }) {
const selectUser = () => {
emit('select', props.user);
};
return {
selectUser
};
}
});
// Using the component
/*
<template>
<UserCard
:user="currentUser"
:is-active="true"
@select="handleSelect"
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import UserCard from './UserCard.vue';
export default defineComponent({
components: { UserCard },
data() {
return {
currentUser: {
id: 1,
name: 'Alice',
email: 'alice@example.com'
} as User
};
},
methods: {
handleSelect(user: User) {
console.log('Selected user:', user);
}
}
});
</script>
*/
Interfaces and State Management
When using state management libraries like Redux or Vuex with TypeScript, interfaces help define the types of actions, mutations, and state.
// Redux example
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoState {
todos: Todo[];
visibilityFilter: string;
}
// Action types
interface AddTodoAction {
type: 'ADD_TODO';
payload: {
id: number;
text: string;
};
}
interface ToggleTodoAction {
type: 'TOGGLE_TODO';
payload: number; // id
}
type TodoAction = AddTodoAction | ToggleTodoAction;
function todosReducer(state: TodoState, action: TodoAction): TodoState {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{
id: action.payload.id,
text: action.payload.text,
completed: false
}
]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
default:
return state;
}
}
Interfaces and API Responses
When handling API responses, interfaces help define the expected data structure, ensuring type safety and improving code maintainability.
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: {
code: number;
message: string;
};
timestamp: string;
}
interface User {
id: number;
username: string;
email: string;
createdAt: string;
}
async function fetchUser(userId: number): Promise<ApiResponse<User>> {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
// Usage
fetchUser(1).then(response => {
if (response.success && response.data) {
console.log(`User: ${response.data.username}`);
} else if (response.error) {
console.error(`Error: ${response.error.message}`);
}
});
Interfaces and Configuration Objects
Interfaces are well-suited for defining the shape of configuration objects, ensuring the type correctness and completeness of configuration options.
interface AppConfig {
apiBaseUrl: string;
maxRetryAttempts?: number;
timeout?: number;
logging?: {
level: 'error' | 'warn' | 'info' | 'debug';
format: 'json' | 'text';
};
}
function initializeApp(config: AppConfig) {
const fullConfig: Required<AppConfig> = {
maxRetryAttempts: 3,
timeout: 5000,
logging: {
level: 'info',
format: 'text'
},
...config
};
console.log('Initializing app with config:', fullConfig);
// Initialization logic...
}
initializeApp({
apiBaseUrl: 'https://api.example.com',
logging: {
level: 'debug',
format: 'json'
}
});
Interfaces and Type Guards
Combining interfaces with type guards creates more powerful type-checking mechanisms, especially when dealing with union types.
interface Circle {
kind: 'circle';
radius: number;
}
interface Square {
kind: 'square';
sideLength: number;
}
interface Triangle {
kind: 'triangle';
base: number;
height: number;
}
type Shape = Circle | Square | Triangle;
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.sideLength ** 2;
case 'triangle':
return (shape.base * shape.height) / 2;
default:
// Ensure all cases are handled
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
const shapes: Shape[] = [
{ kind: 'circle', radius: 5 },
{ kind: 'square', sideLength: 10 },
{ kind: 'triangle', base: 4, height: 3 }
];
shapes.forEach(shape => {
console.log(`${shape.kind} area: ${getArea(shape)}`);
});
Interfaces with Mapped Types and Conditional Types
TypeScript's advanced type
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn