TypeScript currently has no mechanism to narrow the type of an object by checking the type of one of its properties (except if the object is a discriminated union and the property being checked is the discriminant). There's a longstanding open feature request for this at microsoft/TypeScript#42384, but for now it's not part of the language. You can write a user-defined type guard function to simulate this behavior for some use cases, but there doesn't seem to be a one-size-fits-all solution.
In the case where the object is of a (non-discriminated) union type and you want to check one of its (non-discriminant) properties, one such user-defined type guard function looks like
function hasPropOfType<T, K extends keyof T, V extends T[K]>(
obj: T, key: K, guard: (v: T[K]) => v is V): obj is Extract<T, Record<K, V>> {
return guard(obj[key]);
}
This function, like all user-defined type guard functions, is only as safe as the implementation is correct, and TypeScript cannot and does not try to validate that for you. If you rewrote return Math.random() < 0.5 the code would still compile the same. The point is not that user-defined type guard functions guarantee safety automatically; the point is that the unsafe operation is confined to the implementation of one function, which you can then call multiple times in multiple places and it acts as if the missing feature were really there. It's a user-defined type guard, meaning you can write it yourself instead of waiting for a language feature request to be implemented.
If the requirement is that the language must already have the feature that lets you check source.buffer and have it automatically narrow source, then this is currently impossible. But presumably one wants a way forward despite that which is at least somewhat less ad-hoc than a type assertion.
For what remains I'll assume that calling hasPropOfType() is a viable path forward (if not, one can just stop reading here).
You could use hasPropOfType() if you rewrite your function to accept a union of Uint8Array<⋯> types instead of a generic type:
function fixUint8Array(
source: Uint8Array<ArrayBuffer> | Uint8Array<SharedArrayBuffer> | Uint8Array
): Uint8Array<ArrayBuffer> {
if (hasPropOfType(source, "buffer", x => x instanceof ArrayBuffer)) {
return source
}
const buffer = source.buffer;
const copy = new ArrayBuffer(buffer.byteLength);
new Uint8Array(copy).set(new Uint8Array(buffer));
return new Uint8Array(copy);
}
Here you're checking the buffer property of source using the callback guard x => x instanceof ArrayBuffer, which is inferred as a type predicate returning function automatically for you. So inside the true block of the if statement you get that source has now been narrowed to Uint8Array<ArrayBuffer>. Inside the false block it has not been narrowed (well, it's been narrowed to Uint8Array<SharedArrayBuffer> | Uint8Array<ArrayBufferLike>) and you can copy the buffer as before.
Note that if you wanted to keep it generic you'd have to write an even more special-cased type guard function, because TypeScript does not and has never re-constrained generic type arguments based on type guard checks. Like, the following doesn't work and there's no way for it to work:
function hmm<U>(arr: U[]) {
if (typeof arr[0] === "string") {
arr.map(x => x.toUpperCase()); // error!
}
if (hasPropOfType(arr, 0, x => typeof x === "string")) {
arr.map(x => x.toUpperCase()); // error!
}
}
The ability to check a generic-typed value and have TypeScript re-constrain the generic type argument (e.g., reconstrain T from T extends ArrayBufferLike to T extends ArrayBuffer upon checking source.buffer, or constrain U to U extends string upon checking arr[0]) is another missing feature. The closest I can think of for that one is microsoft/TypeScript#27808, but that one would only help if the constraint were a list of possibilities and not a union. So instead of Uint8Array<T> where T extends ArrayBufferLike, you'd need Uint8Array<T> where T oneof (ArrayBuffer, SharedArrayBuffer) (made-up syntax), so you'd need to refactor your code anyway.
This is something you can't even write a user-defined type guard function to fix in general, since you can't abstract over generics that way. That would require higher-kinded types as discussed in microsoft/TypeScript#1213. Which means that the only way to do it would be to pick a particular generic thing, like, say Uint8Array itself. Leading to the type guard function like
function hasBufferOfType<T extends ArrayBufferLike, U extends T>(
source: Uint8Array<T>, guard: (buffer: T) => buffer is U): source is Uint8Array<U> {
return guard(source.buffer)
}
and the implementation of fixUint8Array like
function fixUint8Array<T extends ArrayBufferLike>(
source: Uint8Array<T>
): Uint8Array<ArrayBuffer> {
if (hasBufferOfType(source, x => x instanceof ArrayBuffer)) {
return source
}
const buffer = source.buffer;
const copy = new ArrayBuffer(buffer.byteLength);
new Uint8Array(copy).set(new Uint8Array(buffer));
return new Uint8Array(copy);
}
This works nicely, but without higher-kinded types it's fairly limited in applicability, and there's less of a chance you'd need to have a separate function for it, especially if you only ever call it inside fixUint8Array. You might then decide to just use a type assertion and comment the function like
function fixUint8Array3<T extends ArrayBufferLike>(
source: Uint8Array<T>
): Uint8Array<ArrayBuffer> {
const buffer = source.buffer;
if (buffer instanceof ArrayBuffer) {
return source as Uint8Array<ArrayBuffer>; // TS can't figure this out itself
}
const copy = new ArrayBuffer(buffer.byteLength);
new Uint8Array(copy).set(new Uint8Array(buffer));
return new Uint8Array(copy);
}
Which is against the requirement of the question here.
Playground link to code
as unknown as Toras any as T, though your linter might complain.