Skip to Content
TypeScript Intermediate: Types That Survive Real Projects
Intermediate
0 likes
Courses
TypeScript Intermediate: Types That Survive Real Projects

Generic Functions That Keep the Input Type

Write generic functions with one or two type parameters, limit them with extends and drop the ones that link nothing. After this, a function can accept many types and still hand back the right one.

Course content

Sign in to save your progress.

Course Home

Skip ahead: If you already write function f<T>(x: T): T and know what extends does in <T extends ...>, skip to "Don't add a type parameter you use once", then continue.

A function that takes anything and gives back anything is easy to write and hard to trust. The usual fix is any, and any switches the checks off for everything that comes out. A generic function keeps the type of what went in, so what comes out is still checked. That is the whole idea. The syntax is the only part that looks scary.

The problem: any forgets the type

function firstOf(items: any[]): any {
  return items[0];
}

const price = firstOf([19.5, 20, 7]);
console.log(price.toUpperCase());

This compiles with no error. The numbers go in, any comes out, and the compiler no longer knows price is a number. Run it and you get a TypeError for calling toUpperCase on a number. The mistake is obvious here. In a real project the call and the use are in different files.

A type parameter is a variable for a type

T is a name for "whatever type the caller passes in". You declare it in angle brackets after the function name and use it where the real type would go.

function identity<T>(value: T): T {
  return value;
}

const word = identity("hello");
const count = identity(42);
const flag = identity<boolean>(true);

console.log(word.toUpperCase(), count.toFixed(1), flag);
HELLO 42.0 true

You did not write <string> on the first call. The compiler worked it out from the argument, the same way it infers a variable's type. That is called inference, and it is how you will call almost every generic function. Write the angle brackets yourself only when inference cannot decide, like an empty array you want typed.

Diagram: identity of

Keep the element type

Here is the firstOf from above, fixed.

function firstOf<T>(items: T[]): T | undefined {
  return items[0];
}

const price = firstOf([19.5, 20, 7]);
price.toFixed(1);
first_of.ts(6,1): error TS18048: 'price' is possibly 'undefined'.

Two things happened. price is a number | undefined, not any, so a wrong method would be an error. And the compiler also reminds you that an empty array has no first item. Check for undefined before you use it, the same way you would for an optional property.

Two type parameters

A function that turns one kind of thing into another needs two names.

function mapAll<T, U>(items: T[], fn: (item: T) => U): U[] {
  const out: U[] = [];
  for (const item of items) {
    out.push(fn(item));
  }
  return out;
}

const lengths = mapAll(["a", "bb", "ccc"], (s) => s.length);
const labels: string[] = lengths;
console.log(lengths);
map_all.ts(10,7): error TS2322: Type 'number[]' is not assignable to type 'string[]'.
  Type 'number' is not assignable to type 'string'.

T comes from the array (string) and U comes from what the callback returns (number). Because of that, s in the callback is a string with no annotation, and lengths is a number[]. The error is the compiler refusing to treat numbers as labels. This is the same map you use on arrays, which is declared with a type parameter in a very similar way.

Limit what T can be

Sometimes the function needs to do something with the value, like read length. An unrestricted T has no properties at all, so say what it must have.

function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

console.log(longest("ab", "c"));
console.log(longest([1, 2], [3]));
longest(10, 20);
longest.ts(7,9): error TS2345: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

T extends { length: number } means "any type, as long as it has a numeric length". Strings and arrays qualify. Numbers do not. And the return type is still T: for the string call you get a string, for the array call a number[].

function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

console.log(longest("ab", "c"));
console.log(longest([1, 2], [3]));
ab
[ 1, 2 ]

Don't add a type parameter you use once

function logValue<T>(value: T): void {
  console.log(value);
}

function logValueBetter(value: unknown): void {
  console.log(value);
}

logValue("a");
logValueBetter("a");

Both compile. T appears one time, so it connects nothing. It does not link an input to an output or two inputs to each other. My rule: if a type parameter shows up in only one place, take it out. A generic is worth the extra syntax when it links two places.

⚠ Warning: Do not write T and then cast inside the function, like return value as T on something that is not a T. That is any with extra steps. If you need a cast to make a generic function compile, the signature is probably promising more than the body delivers.

Common mistakes

  • Writing a generic when a plain union would do. string | number is simpler if there are only two cases.
  • Using any for the return type and calling the function generic because it has angle brackets.
  • Naming type parameters with one letter in a long signature. T and U are fine for two, but use Item and Result when you have more than that.
  • Passing the type by hand every time. If you always write <string>, the compiler is probably missing something it could infer.

💡 Tip: When a generic signature confuses you, read it as a sentence: "for any type T, take a list of T and return a T or undefined".

If you skip this, you will use any the moment a function should work for two types, and the wrong type will travel with it. Which of your helper functions returns any right now, and could the answer be a T instead?

About

Write generic functions with one or two type parameters, limit them with extends and drop the ones that link nothing. After this, a function can accept many types and still hand back the right one.

Duration
5 min
Last update
10/22/2026
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.