Execute Program

TypeScript Basics: Object Narrowing

Welcome to the Object Narrowing lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • We can assign object types to "smaller" object types. This is called narrowing: we narrow the "larger" type to the "smaller" type. For example, we might have a full user type:

  • >
    type User = {
    email: string
    admin: boolean
    };
    let amir: User = {
    email: 'amir@example.com',
    admin: true,
    };
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    amir.email;
    Result:
    'amir@example.com'Pass Icon
  • But sometimes we might not care about the whole user. Maybe sometimes we only care about the email address.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    type HasEmail = {
    email: string
    };
    let amirEmail: HasEmail = amir;
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    amirEmail.email;
    Result:
    'amir@example.com'Pass Icon
  • When we do this, the 'smaller' type loses knowledge of where it came from. For example, our HasEmail type doesn't know about the admin property.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    amirEmail.admin;
    Result:
    type error: Property 'admin' does not exist on type 'HasEmail'.Pass Icon
  • Notice that in the example above, we didn't say that User and HasEmail are related. TypeScript automatically knows that they're related because HasEmail is a "smaller" version of User. User has an email and an admin flag, but HasEmail only has an email.

  • This is called structural typing: the object's structure determines the type. Some other programming languages would require us to explicitly say "a User is a bigger kind of HasEmail." That would be nominal typing and is not supported by TypeScript.

  • Narrowing can be very useful. For example, our system might have a sendEmail method. If it takes a User, then we'll need a full user to call it.

  • However, a sendEmail that takes a {email: string} is more flexible. We can pass it a user, or we can pass it just an {email: string} to send an email to an address not associated with a user.

  • >
    type User = {email: string, admin: boolean};

    let amir: User = {email: 'amir@example.com', admin: true};

    function sendEmail({email}: {email: string}): string {
    return `Emailing ${email}`;
    }

    [
    sendEmail(amir),
    sendEmail({email: 'betty@example.com'}),
    ];
    Result:
    ['Emailing amir@example.com', 'Emailing betty@example.com']Pass Icon
  • Finally, a note on terminology. Structural typing is also sometimes called "duck typing". This term is a reference to a saying: "If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck."

  • In programming, "duck typing" means that we only care about the properties we're accessing. We don't care whether the type's name is User or HasEmail, and we don't care whether the object has extra properties.