Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Proposal] Type assertion statement (type cast) at block-scope level #10421

Open
yahiko00 opened this issue Aug 18, 2016 · 84 comments
Open

[Proposal] Type assertion statement (type cast) at block-scope level #10421

yahiko00 opened this issue Aug 18, 2016 · 84 comments
Labels
Revisit An issue worth coming back to Suggestion An idea for TypeScript

Comments

@yahiko00
Copy link

yahiko00 commented Aug 18, 2016

This is a proposal in order to simplify the way we have to deal with type guards in TypeScript in order to enforce the type inference.

The use case is the following. Let us assume we have dozens (and dozens) of interfaces as the following:

Code

interface AARect {
    x: number; // top left corner
    y: number; // top left corner
    width: number;
    height: number;
}

interface AABox {
    x: number; // center
    y: number; // center
    halfDimX: number;
    halfDimY: number;
}

interface Circle {
    x: number; // center
    y: number; // center
    radius: number;
}

// And much more...

And we have a union type like this one:

type Geometry = AARect | AABox | Circle | // ... And much more

It is quite easy to discriminate a type from another with hasOwnProperty or the in keyword:

function processGeometry(obj: Geometry): void {
    if ("width" in obj) {
        let width = (obj as AARect).width;
        // ...
    }
    if ("halfDimX" in obj) {
        let halfDimX = (obj as AABox).halfDimX;
        // ...
    }
    else if ("radius" in obj) {
        let radius = (obj as Circle).radius;
        // ...
    }
    // And much more...
}

But, as we can see, this is quite burdensome when we need to manipulate obj inside each if block, since we need to type cast each time we use obj.

A first way to mitigate this issue would be to create an helper variable like this:

    if ("width" in obj) {
        let helpObj = obj as AARect;
        let width = helpObj.width;
        // ...
    }

But this is not really satisfying since it creates an artefact we will find in the emitted JavaScript file, which is here just for the sake of the type inference.

So another solution could be to use user-defined type guard functions:

function isAARect(obj: Geometry): obj is AARect {
    return "width" in obj;
}

function isAABox(obj: Geometry): obj is AABox {
    return "halfDimX" in obj;
}

function isCircle(obj: Geometry): obj is Circle {
    return "radius" in obj;
}

// And much more...

function processGeometry(obj: Geometry): void {
    if (isAARect(obj)) {
        let width = obj.width;
        // ...
    }
    if (isAABox(obj)) {
        let halfDimX = obj.halfDimX;
        // ...
    }
    else if (isCircle(obj)) {
        let radius = obj.radius;
        // ...
    }
    // And much more...
}

But again, I find this solution not really satisfying since it still creates persistent helpers functions just for the sake of the type inference and can be overkill for situations when we do not often need to perform type guards.

So, my proposal is to introduce a new syntax in order to force the type of an identifier at a block-scope level.

function processGeometry(obj: Geometry): void {
    if ("width" in obj) {
        assume obj is AARect;
        let width = obj.width;
        // ...
    }
    if ("halfDimX" in obj) {
        assume obj is AABox;
        let halfDimX = obj.halfDimX;
        // ...
    }
    else if ("radius" in obj) {
        assume obj is Circle;
        let radius = obj.radius;
        // ...
    }
    // And much more...
}

Above, the syntax assume <identifier> is <type> gives the information to the type inference that inside the block, following this annotation, <identifier> has to be considered as <type>. No need to type cast any more. Such a way has the advantage over the previous techniques not to generate any code in the emitted JavaScript. And in my opinion, it is less tedious than creating dedicated helper functions.

This syntax can be simplified or changed. For instance we could just have :
<identifier> is <obj>
without a new keyword assume, but I am unsure this would be compliant with the current grammar and design goals of the TypeScript team.
Nevertheless, whatever any welcomed optimization, I think the general idea is relevant for making TypeScript clearer, less verbose in the source code and in the final JavaScript, and less tedious to write when we have to deal with union types.

@DanielRosenwasser DanielRosenwasser added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Aug 18, 2016
@DanielRosenwasser
Copy link
Member

Technically we could just consider in as a form of type guards.

But I can still imagine something that says "assume the type of this entity is so-and-so for this block".

@yahiko00
Copy link
Author

yahiko00 commented Aug 18, 2016

That would be nice if the in keyword could be a form of type guard.
Although, I am unsure it would be easy to handle such a following case with type inference only:

interface A {
    x: number;
    z: number;
}

interface B {
    y: number;
    z: number;
}

interface C {
    x: number;
    y: number;
}

type Union = A | B | C;

function check(obj: Union): void {
    if ("x" in obj && "z" in obj) {
        // obj is now treated as an instance of type A
    }
    else if ("y" in obj && "z" in obj) {
        // obj is now treated as an instance of type B
    }
    else if ("x" in obj && "y" in obj) {
        // obj is now treated as an instance of type C
    }
}

If such a case with combination of in cannot be handled in a short term, the single discriminating in as a form of type guard would be nice though for many use cases.
If combination of in can be handled anytime soon, the syntax assume <identifier> is <type> would still be relevant in general cases since it is a factored version of inline type cast.

@yortus
Copy link
Contributor

yortus commented Aug 19, 2016

This looks like the same concept as #9946, but with different syntax.

@yahiko00
Copy link
Author

yahiko00 commented Aug 19, 2016

@yortus Thanks for the reference. Yes, I am glad I am not the only one who felt the need of such a concept even though the previous proposal insisted on type narrowing. What I propose is more than type narrowing, but type overcasting.
For instance, let us assume a variable obj has a type A, whatever this type is at a given point of the code. We would be able force its type to B with the new syntax (or another, I don't really care),

let obj: A; // Type of obj is A
// ...
assume obj is B; // Type of obj is B
// ...

This is would not only apply to type narrowing, but also to type casting. As said before, we could consider this proposal as a way to type cast an identifier at a block-scope level. Thinking of that, I am wondering if this could not be better to use as instead of is in order to underline the connection between this proposal and inline type cast:
assume obj as B;

@yahiko00 yahiko00 changed the title [Proposal] Type guards made it easy(ier) [Proposal] Type cast at block-scope level Aug 19, 2016
@yortus
Copy link
Contributor

yortus commented Aug 19, 2016

So what you are really proposing is a type assertion statement that is otherwise exactly like the existing type assertion expression. For example:

function fn(arg: P) {...}

// Type assertion as an expression (existing syntax)
let x: Q;
f(x as P); // 'x as P' is a type assertion expression

// Type assertion as a statement (proposed new syntax)
let y: Q;
assume y is P; // this is a type assertion statement
f(y); // y has type P here

As with other statement/expression equivalences, this proposed new form of type assertion is used for its side-effects rather than its result, although obviously these are compile-time side-effects, since type assertions are elided at runtime. And as you mention you want the side-effects limited to the block in which the assertion appears.

@yahiko00
Copy link
Author

yahiko00 commented Aug 19, 2016

@yortus I like your distinction between statement and expression. So yes, type assertion statement is probably the best way to name this proposal.

Syntactic possibilities:

1. As suggested in #9946: declare <identifier>: <type>.
The use of keyword declare is interesting since it already exists, and emphasizes on the fact this is an annotation that will not be emitted in the JavaScript. However, it makes me think more of ambient declarations and could suggest we do not have an instanciation of <identifier> before, which is misleading. Also, I am quite afraid of conflicts with existing use cases of declare.

2. My primary proposal: assume <identifier> is <type>.
It adds a new keyword to existing ones which can be seen as undesirable. However, I think we need a new keyword to express this new concept of type assertion statement. I first suggested the keyword is to introduce the type, since I had type guards in mind at the begining. But in fact, it is less related to type guard than type assertion (type cast).

3. Type assertion statement syntax: assume <identifier> as <type>.
This my favorite syntax up to now. The as keyword, instead of is, underline the connection with its inline counterpart, the type assertion expression.

4. Short syntax A: <identifier> is <type>
This syntax was suggested in my original proposal but it looks like more than an expression than a statement. And such a syntax could be more relevant for future proposals where type guards are directly involved.

5. Short syntax B: <identifier> as <type>
This syntax would lead to a conflict with type assertion expression. I do not think the current grammar to be able to distinguish a type assertion expression from a type assertion statement written this way, since it is today perfectly valid to write a type assertion expression like the following code:

let obj: any;
obj as number; // Type assertion expression

So my preference goes for syntax (3): assume <identifier> as <type>

Extra notes

  • Type can be any valid type, including union types.
  • A new type assertion statement for a same identifier overrides previous ones. For instance:
let obj: A; // Type of obj is A
...
assume obj as B; // Type of obj is B
...
assume obj as C; // Type of obj is C
...

@yahiko00 yahiko00 changed the title [Proposal] Type cast at block-scope level [Proposal] Type assertion statement (type cast) at block-scope level Aug 19, 2016
@yortus
Copy link
Contributor

yortus commented Aug 19, 2016

For reference, the spec on type assertion expressions (section 4.16) is here.

The rules and use-cases described there for type assertions would presumably apply to this proposal in exactly the same way.

@yortus
Copy link
Contributor

yortus commented Aug 19, 2016

BTW I see you're only proposing this to work on simple identifiers, whereas type assertion expressions work on arbitrary expressions, like foo.bar as string. Type guards also work with properties, like if (isString(foo.bar)) {foo.bar.length}. It might be useful to consider at least some expressions as valid in type assertion statements, for example assert foo.bar as string;

@yahiko00
Copy link
Author

In my mind, <identifier> included both simple variable identifiers and properties. But after checking the grammar specifications, I should have been more precise. ;)
So yes, both identifiers and properties should be allowed in type assertion statements.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Aug 19, 2016

But again, I find this solution not really satisfying since it still creates persistent helpers functions just for the sake of the type inference and can be overkill for situations when we do not often need to perform type guards.

@yahiko00 User defined type guard functions actually are an important part of the emitted code. They may determine control flow at runtime. Also this is not type inference, it is type assertion (or type assumption 😛 ). If you want shorthand type guards you can write

function is<T>(value, condition: boolean): value is T {
    return condition;
}

then you can write the following

function processGeometry(obj: Geometry): void {
    if (is<AARect>(obj, "width" in obj)) {
        let width = obj.width;
        // ...
    }
    if (is<AABox>(obj, "halfDimX" in obj)) {
        let halfDimX = obj.halfDimX;
        // ...
    }
    else if (is<Circle>(obj, "radius" in obj)) {
        let radius = obj.radius;
        // ...
    }
}

@yahiko00
Copy link
Author

yahiko00 commented Aug 19, 2016

I do not deny the usefulness of type guard functions. But there are cases where I do not want type guards functions. First, as expressed before, because it adds verbosity where it is not needed. Your workaround above is interesting but adds a layer of abstraction, which is less clear than a plain test if("width" in obj), in my opinion. You still need a persistent helper function to perform type inference. Said in a different manner, I want the language to help me, not being on my way.
Also, even if I know performance is not the priority of the TypeScript language, unfortunately, I feel better when I can minimize function calls in interpreted languages, especially in situations where speed is important, like in video games.

@aluanhaddad
Copy link
Contributor

Fair enough.
if("width" in obj) could perhaps be a type guard in a future version of the language but the inferred type could not really be AARect. Rather it would have to be something like { width: any }.

@yortus
Copy link
Contributor

yortus commented Aug 20, 2016

@aluanhaddad if("width" in obj) could narrow obj to AARect if the declared type of obj is a union like AARect | AABox | Circle.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Aug 20, 2016

@yortus that's an excellent point

@basarat
Copy link
Contributor

basarat commented Aug 21, 2016

Consider using a discriminated union with a type literal https://basarat.gitbooks.io/typescript/content/docs/types/discriminated-unions.html 🌹

@yahiko00
Copy link
Author

That is true.
But I see a drawback to this approach since it adds an helper property which could be bigger in size (bytes) than the sum of the others. If I have to manipulate thousands or tens thousands of object, this is a luxury I cannot afford in some situations.
Also, it is not always possible to use an helper property if structures behind interfaces do not implement such property and we do not have the possibility (lack of rights or access to the source code) to do so.

@basarat
Copy link
Contributor

basarat commented Aug 21, 2016

FWIW I asked for this a long time ago but once we got let/const I've just done the following (before discriminated unions):

interface AARect {
    x: number; // top left corner
    y: number; // top left corner
    width: number;
    height: number;
}

interface AABox {
    x: number; // center
    y: number; // center
    halfDimX: number;
    halfDimY: number;
}

interface Circle {
    x: number; // center
    y: number; // center
    radius: number;
}
type Geometry = AARect | AABox | Circle; // ... And much more

function processGeometry(obj: Geometry): void {
    if ("width" in obj) {
        let objT = obj as AARect;
        let width = objT.width;
        // ...
    }
    if ("halfDimX" in obj) {
        let objT = obj as AABox;
        let halfDimX = objT.halfDimX;
        // ...
    }
    else if ("radius" in obj) {
        let objT = obj as Circle;
        let radius = objT.radius;
        // ...
    }
    // And much more...
}

🌹

@RyanCavanaugh RyanCavanaugh added Revisit An issue worth coming back to and removed In Discussion Not yet reached consensus labels Aug 22, 2016
@RyanCavanaugh
Copy link
Member

Logged #10485 because we always prefer to just have narrowing happen automatically.

We've wanted a syntactic space for this for a while but haven't found anything that isn't ugly or looks like an expression with side effects.

@daprahamian
Copy link

@shicks Thanks! I have seen that workaround, and it mostly works, but does technically break Function.prototype.length for the function. I think in the spirit of this feature request, the desire is for Typescript code to completely compile away. In general, having to write code differently not b/c it is bad practice, but b/c the type engine cannot understand it, is a very frustrating dev experience.

I think having a block-scoped assertion mechanism would really help to act as a stopgap for this and other edge cases in the typing system. As the typing system gets more and more sophisticated, it can be used less and less.

@Ayub-Begimkulov
Copy link

Ayub-Begimkulov commented Mar 29, 2021

Is it time to close this issue? Between discriminated unions and asserts functions, it seems like this is pretty well-covered, and it's doubtful enough of the original use case remains for any solution to be worth its complexity cost.

But what about the case with functions that mutate the properties of an exciting object?
Right now you can do something like this:

function setB<T extends Record<string, any>>(obj: T): asserts obj is T & { b: number[] } {
  (obj as any).b = [1, 2, 3]
} 

But the problem is that if the object already property b with type string, then the type of obj.b will be intersection of types string and number[]:

const myObject = { b: 'hello world' };

setB(myObject);

type BType = typeof myObject.b // string & number[]

Which could lead to unnecessary bugs. I tried to create my function like this:

type SetB<T extends Record<string, any>> = {
  [K in (keyof T | 'b')]: K extends 'b' ? number[] :T[K] 
}

function setB<T extends Record<string, any>>(obj: T): asserts obj is SetB<T> {
  (obj as any).b = [1, 2, 3]
} 

But it compiles with type error:

A type predicate's type must be assignable to its parameter's type.

I'm not trying to say that this syntax must be added to the language. But, unfortunately, right now I don't see any way to correctly type functions that mutate objects, and in my opinion, something needs to be done about it.

@EnderShadow8
Copy link

EnderShadow8 commented Jul 7, 2021

I found an absurd workaround for this issue. For some reason, using a @ts-ignore before a declare still declares the variable type, but suppresses the warning that Modifiers cannot appear here.

In fact, this could be a possible syntax if this suggestion were to be implemented, since it doesn't create any new keywords.

interface A {
    x: number;
    z: number;
}

interface B {
    y: number;
    z: number;
}

interface C {
    x: number;
    y: number;
}

type Union = A | B | C;

function check(obj: Union): void {
    if ("x" in obj && "z" in obj) {
        // @ts-ignore
        declare let obj: A

        // obj is now treated as an instance of type A
    }
    else if ("y" in obj && "z" in obj) {
        // @ts-ignore
        declare let obj: B

        // obj is now treated as an instance of type B
    }
    else if ("x" in obj && "y" in obj) {
        // @ts-ignore
        declare let obj: C

        // obj is now treated as an instance of type C
    }
}

EDIT:

This only works in a new scope, for example inside a block. Usually, since this is useful to augment type narrowing, this is not an issue. However, casting variables such as function arguments using let produces a duplicate declaration error, and using var simply fails silently.

@mindplay-dk
Copy link

mindplay-dk commented Jul 8, 2021

@EnderShadow8 this is interesting 🙂

I did some tests to see if I could break it, but it seems to work - my main worry was the redeclared types would somehow leak to the .d.ts file, but that does not appear to be the case, and IDE support looks sound as well.

I couldn't find any gotchas - is this really as simple as just lifting the restriction and allowing what already works? 🤷‍♂️

It should probably still have a dedicated syntax though, since let seems to imply we're actually declaring a new variable with the same name, shadowing a variable in the parent block scope, which isn't really what's happening. Although, in practical terms (in terms of typing) I suppose the effects of doing that would be exactly the same.

If nothing else, this makes for a pretty nifty workaround. 😃👍

EDIT: the only caveat I can find is the fact that the new symbol is just that: a new symbol, shadowing the symbol on the parent block scope - which means, your inner obj is an entirely different symbol, disconnected from obj in the parent scope, as you can see if you use "find references" ... as you can see in the example here, there's no relationship with the declaration on line 18:

image

So the risk of doing this, is it will break if you rename or remove the parent symbol - and emit code that fails.

@frank-weindel
Copy link

Just wanted to drop another use case for this that I documented on StackOverflow.

I came up with a similar solution as @MattiasBuelens's assumeType<T> assertion. But it still would be nice not to have to rely on tree shaking/dead-code elimination to achieve this without runtime consequences. Especially since that elimination could be prevented by the presumptive possibility of side effects in some cases.

@shicks
Copy link
Contributor

shicks commented Nov 16, 2022

FWIW, I'd be surprised if even the most basic optimizer can't recognize and eliminate calls to a no-op function:

$ echo 'function noop(){} console.log(1); noop(); console.log(2);' | npx esbuild --format=iife --minify
(()=>{console.log(1);console.log(2);})();

@frank-weindel
Copy link

@shicks No but you can easily end up with a case where maybe the function call is eliminated but the argument is not due to the possibility of side effects.

$ echo 'class MyClass { prop = 123 } const myClass = new MyClass(); function noop(arg){} console.log(1); noop(myClass.prop); console.log(2);' | npx esbuild --format=iife --minify
(()=>{class o{prop=123}const s=new o;console.log(1);s.prop;console.log(2);})();

@mnpenner
Copy link

I just use babel-plugin-unassert. That both informs TS and gives me runtime assertions in dev but removes them for prod.

@davidglezz
Copy link

davidglezz commented Dec 18, 2022

I think that this issue can be closed because Typescript already narrows the type correctly (at least since version 3.3, which is the last one that we can test in the playground).

image

Playground

@vegerot
Copy link

vegerot commented Jan 3, 2023

I love this idea. This thread is really long but what're the current blockers?

@jacekkopecky
Copy link

With the new satisfies keyboard, I'd love to be able to write code like this:

interface AOptional {
  a?: number,
  // ...
}

interface ARequired {
  a: number,
  newProp?: string,
  // ...
}

function convertToARequiredInPlace(obj: AOptional): ARequired {
  obj.a ??= 0;
  obj satisfies ARequired; // this currently fails but the compiler could see that the line above fixes that

  // then TS could start treating obj as ARequired and these would both be allowed
  obj.newProp = "foo"; 
  return obj;
}

@tjzel
Copy link

tjzel commented Jan 31, 2024

Why hasn't this been included for so many years? Reanimated in React-Native really could use it - we have to face multiple architectures, platforms and it would be nice if after checking what platform we are on we could inline type guard some parameters to be platform specific. Since performance is key for us even a couple type guard calls can be too expensive and multiple type assertions are just cluttering the code.

@shicks
Copy link
Contributor

shicks commented Feb 1, 2024

Why hasn't this been included for so many years? Reanimated in React-Native really could use it - we have to face multiple architectures, platforms and it would be nice if after checking what platform we are on we could inline type guard some parameters to be platform specific. Since performance is key for us even a couple type guard calls can be too expensive and multiple type assertions are just cluttering the code.

If you write a no-op type guard, you shouldn't see any performance regression. Nearly every VM will do JIT optimization to inline the empty function call into the calling functions' bytecode. The only exception I'm aware of is iOS native, which doesn't allow it, but in that case you're already bundling, and every bundler can (and will) also inline empty functions in production builds. Such an empty type guard isn't particularly "safe" (since it's not actually doing any checking at runtime), but depending on how you type it, it's no less safe than an ordinary as T type assertion that you'd write without it.

See https://jsbench.me/00ls2rex1g/1

@dead-claudia
Copy link

Also, you can get 90% of the way there with value = value as T (it works most of the time but not all of the time), and that's something that can be statically dropped without even needing to do lexical analysis first.

@tjzel
Copy link

tjzel commented Feb 1, 2024

@shicks
That's a very useful concept, but imo makes the code less readable.

variable is Type;

seems to be much more informative than

noopAssertJustToConfirmScopeType(variable);

But of course the function can be named better etc. and some guidelines can be added, so that's just an opinion.

Unfortunately, we cannot exactly rely on JIT optimizations since what we actually do is copying JS functions in between JS runtimes via their code (as a string).

I know it sounds dumb, but currently it's the only option we have, since we must do it in runtime. We are looking into the possibility of having this code copied before the runtime is started, so we could actually have those JIT optimizations (along many other things) included, but at the moment we aren't exactly sure if it's feasible and it requires effort not only from us but also from folks from React Native.

Maybe this comes from my misunderstanding of how TypeScript should be used - I always considered it to be an augmentation of JavaScript - that means, if I have a JS function that is dynamically type safe, I can make its TypeScript counterpart statically type-safe, compile it and get exactly the same code as the original JS function. If that's not the mission of TypeScript, I'm completely fine with that and the feature proposed here isn't necessary.

@dead-claudia
I definitely agree that I can get away with simple type assertions but I'm talking here more about code readability.

From time to time we have some really "dense" core functions, some algorithms that are very concise and have to take into account various platforms. Once you know the code there's no problem skipping as Type when you read it, but when some new member of the team gets to work with it and he sees multiple type assertions everywhere it can make the process of understanding the code more difficult.

You might argue that in this case the function is poorly written in TypeScript - and I agree with you, back in the day, the code was purely JS and rewriting it into TS wasn't the simplest of tasks. I try to tackle down such functions and rewrite them to be of equivalent performance and behavior but with better types. However, since they are core functions, it requires a lot of attention from the whole team to make sure there aren't any regressions involved in those type of refactors, since there are many little details that could be overlooked. Therefore such inline type guards would be a good QoL addition.

@lostpebble
Copy link

@tjzel I agree 100% that something like this in the language would be amazing- our comments are almost 1 to 1 (see earlier in the thread).

I've found that this is probably the simplest way to do this as of now:

function cast<T>(it: any): T {
  return it;
}

function castTest() {
  const testObject = {
    color: "red",
  };
  
  // asString is now a string type (obviously bad though)
  const asString = cast<string>(testObject);
}

The syntax isn't so bad- and feels natural enough.

const color = cast<Color>(someColorThing);

But yea, having to do this could be dangerous overall and remove some of the type safety that TypeScript gives to us. But there definitely are places where this comes in handy (not every project is greenfield).

@jdom
Copy link
Member

jdom commented Feb 2, 2024

@lostpebble your last workaround doesn't really address this. The whole purpose of this issue is to make an existing variable be inferred as a different type, not create a new variable nor make function calls that impact runtime. Otherwise the simplest is:

const color = someColorThing as Color;
const asString = testObject as unknown as string;

But again, the whole purpose is to avoid that runtime change entirely.

@oscarhermoso
Copy link

Just sharing another scenario where this would be super helpful - receiving an object as a parameter, extending it, and then returning it as the new type, without assigning any new variables.

Given a scenario of building a JSON response, this is currently my code:

export async function assign_file_url<F extends FileProperties>(file: F) {
	if (file.kind === 'file') {
		// @ts-expect-error: 'url' does not exist, type is cast at return
		file.url = await storage.bucket(file.bucketId).presignGetUrl(file.publicId);
	} else {
		// @ts-expect-error: 'url' does not exist, type is cast at return
		file.url = null;
	}

	return file as F & { url: string | null };
}

And this could be the code after an "assume" keyword is introduced.

export async function assign_file_url<F extends FileProperties>(file: F) {
	assume file as F & { url: string | null };
	
	if (file.kind === 'file') {
		file.url = await storage.bucket(file.bucketId).presignGetUrl(file.publicId);
	} else {
		file.url = null;
	}

	return file;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Revisit An issue worth coming back to Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests