Typescript: Change type of object property depended on the properties name

This does what you want, but it involves a couple of type assertions. Your original function modified the object that was passed in in-place and then returned that object. This version creates a new object and leaves the original alone. I believe the type assertions are necessary to "build up" the returned object as we iterate through the keys. (If there's safer way to do this that doesn't involve type assertions, I'd be interested to know.)

I updated your example usage at the bottom to demonstrate that this construction works with recursive objects, and used a modified version of @jcalz's Expand type that removes all the properties of Date itself in the final object. This is just for demonstration purposes to clearly show the shape of the final object.

const isObject = (obj: unknown) =>
  (typeof obj === "function" || (typeof obj === "object" && !!obj)) &&
  Array.isArray(obj) === false &&
  !(obj instanceof Date);

type VivifyDates<T> = {
  [K in keyof T]: K extends `${string}_at`
    ? T[K] extends string
      ? Date
      : T[K]
    : VivifyDates<T[K]>;
};

const dateStringsToDate = <T extends { [index: string]: any }>(
  o: T
): VivifyDates<T> => {
  const converted = {} as any;

  for (const key of Object.keys(o)) {
    if (key.endsWith("_at") && typeof o[key] === "string") {
      converted[key] = new Date(o[key]);
    } else {
      converted[key] = o[key];
    }

    if (isObject(o[key])) {
      converted[key] = dateStringsToDate(o[key]);
    }

    if (Array.isArray(o[key])) {
      converted[key] = o[key].map((x: any) => dateStringsToDate(x));
    }
  }

  return converted as VivifyDates<T>;
};

// example usage
const post = {
  created_at: "2022-01-15T14:31:05.252Z",
  title: "foo",
  array: [1, 2, { created_at: "2022-01-15T14:31:05.252Z" }],
  nested: {
    created_at: "2022-01-15T14:31:05.252Z",
    title: "foo",
  },
};
const convertedPost = dateStringsToDate(post);

// Shows the "expanded" type including each field with its final type.
type ExpandRecursively<T> = T extends object
  ? T extends infer O
    ? {
        [K in keyof O]: O[K] extends Date ? O[K] : ExpandRecursively<O[K]>;
      }
    : never
  : T;
const expanded: ExpandRecursively<typeof convertedPost> = convertedPost;

TS Playground