#generic function return type narrowing

58 messages · Page 1 of 1 (latest)

earnest grove
#

first off, im trying to setup a simplified example in the playground but im not getting the same results there compared to my local setup. why isnt the return type of getGlobalConfigProperty

Attributes | Styles | Options
?

prime lightBOT
#
export const editorConfigProperties = [
  "attributes",
  "styles",
  "options",
] as const;

type Attributes = {}
type Styles = {}
type Options = {}
type Config = {
  attributes?: Attributes;
  styles?: Styles;
  options?: Options;
}

declare const config: Config
declare const tabSelection: number

const handleStylesConfigChanged = (eventConfig: {}, config: Styles) => { return {...eventConfig,...config} }
const propertyKey = editorConfigProperties[tabSelection];

const getGlobalConfigProperty = (
//    ^? - const getGlobalConfigProperty: (configProperty: (typeof editorConfigProperties)[number]) => Attributes
  configProperty: typeof editorConfigProperties[number],
) => {
  return config?.[configProperty] ?? {}
};
earnest grove
#

locally i get this:

const getGlobalConfigProperty: (configProperty: (typeof editorConfigProperties)[number]) => Attributes | Styles | Options
but im trying to get a simple reproducible setup in the playground for my real question about generic return type narrowing.

earnest grove
#

!helper

#

hopefully that doesnt piss people off

vocal edge
#

I get the same than you do in the playground ..

prime lightBOT
#
Quadristan#2590

Preview:```ts
export const editorConfigProperties = [
"attributes",
"styles",
"options",
] as const

type Attributes = {}
type Styles = {}
type Options = {}
type Config = {
attributes?: Attributes
styles?: Styles
options?: Options
}

declare const config: Config
...```

vocal edge
#

Ok so about your question..

configProperty: typeof editorConfigProperties[number],

is of type configProperty: "attributes" | "styles" | "options"

therefore,

config?.[configProperty]

is of type
Config['attributes'] & Config['styles'] & Config['options']

Since they are all equal, and empty, in your example, well, typescript just picks one of them, such as Attributes. Fun fact, on the playground, if you reverse the declaration order of the types Attributes and Styles.. you will get a different "result".

Now if you want to narrow, you need to specify the type of the key

#

But there is a first issue.. you may not be able to assign {} to Attrribute/Styles/Options, so you need for the client to pass the default values. or to create a global config object for default values..

#

Then you can do this.

prime lightBOT
#
Quadristan#2590

Preview:```ts
export const editorConfigProperties = [
"attributes",
"styles",
"options",
] as const

type Attributes = {}
type Styles = {}
type Options = {}
type Config = {
attributes?: Attributes
styles?: Styles
options?: Options
}

declare const config: Config
...```

earnest grove
#

shouldnt attr and attr1 error here because they are not Style types?

prime lightBOT
#
kinghat#7157

Preview:ts ... const attr = propertyKey === "attributes" ? handleStylesConfigChanged( {}, globalPropertyConfig ) : {} ...

earnest grove
#

the reason i was trying to reproduce the playground thing is because in my code the types(slightly differnt names from the playground) for getGlobalConfigProperty1 are returned like i assume they would:

#

sorry for the pictures. tired to avoid that but couldnt get it to show like i am seeing locally.

earnest grove
#

ok so i tried just casting the {} as the type if its undefined but somehow its still carrying around undefined in the return type. which then cant be passed to handleStylesConfigChanged 🤔

prime lightBOT
#
kinghat#7157

Preview:```ts
...
const getGlobalConfigProperty1 = <
TProp extends typeof editorConfigProperties[number]

(
// ^? - const getGlobalConfigProperty: (configProperty: (typeof editorConfigProperties)[number]) => Attributes
configProperty: TProp
): Config[TProp] => {
return (
config?.[configProperty] ?? ({} as Config[TProp])
)
}
...```

vocal edge
#

Yus, it's because it is returning a TConfig[TProp] and in your original Config types, all fields are optional.

You can solve this by using NonNullable

prime lightBOT
#
Quadristan#2590

Preview:```ts
export const editorConfigProperties = [
"attributes",
"styles",
"options",
] as const

type Attributes = {}
type Styles = {}
type Options = {}
type Config = {
attributes?: Attributes
styles?: Styles
options?: Options
}

declare const config: Config
...```

earnest grove
#

oh interesting. is it worse practice to cast in the return as in my function vs the way you did it by adding a parameter and typing it there?

vocal edge
#

Casting is bad when it brings false promises. and in your case, casting {} into anything will be risky: every fields of Attribute/Styles/Config wont be trustable since they can be returned as undefined

#

If you dont want to pass a default value, you can define a default value record.. gimme a minute..

prime lightBOT
#
Quadristan#2590

Preview:```ts
export const editorConfigProperties = [
"attributes",
"styles",
"options",
] as const

type Attributes = {}
type Styles = {}
type Options = {}
type Config = {
attributes?: Attributes
styles?: Styles
options?: Options
}

type DefaultValueType = {
[k in keyof Config]: NonNullable<Config[k]>
}
...```

earnest grove
#

does this apply if im using exactOptionalPropertyTypes?

vocal edge
#

You can click on the playground and tick any typescript flag, to check if it works with your setup 🙂

#

click TS COnfig, look for this, toggle it and check !

earnest grove
#

oh ya, i mean when doing {} as NonNullable <Config[TProp]>, why would i need default values if the optional properties cant be undefined?

vocal edge
#

I let you see what happens in this case

prime lightBOT
#
Quadristan#2590

Preview:```ts
const editorConfigProperties = [
"attributes",
"styles",
"options",
] as const

type Attributes = {}
type Styles = {
fontStyles: {
family: string
size: number
}
}
type Options = {}
type Config = {
attributes?: Attributes
styles?: Style
...```

vocal edge
#

There is no type warning, while at the runtime, you will get a surprise.

earnest grove
#

why is it being casted and set as the return value?

const getGlobalConfigProperty1 = <TProp extends typeof editorConfigProperties[number]>( 
  configProperty: TProp,
): NonNullable<Config[TProp]> => { // <---
  return config?.[configProperty] ?? ({} as NonNullable <Config[TProp]>) // <---
};
vocal edge
#

Can you rephrase your question ? I do not understand what you mean

earnest grove
#

rephrased and annotated

vocal edge
#

well you cannot just turn {} into something that may demand more properties. Either you give it the needed properties, either you cheat by casting it

earnest grove
#

i dont follow, are you talking about the default values? im talking about the return and casting:

const getGlobalConfigProperty1 = <TProp extends typeof editorConfigProperties[number]>( 
  configProperty: TProp,
): NonNullable<Config[TProp]> => {
  return config?.[configProperty] ?? ({} as NonNullable <Config[TProp]>)
};

vs

const getGlobalConfigProperty1 = <TProp extends typeof editorConfigProperties[number]>( 
  configProperty: TProp,
) => {
  return config?.[configProperty] ?? ({} as NonNullable <Config[TProp]>)
};
vocal edge
#

Ah, you can definitely remove the return type of the function if you want ! But you will still need to cast, because {} is not a NonNullable<Config[TProp]>

earnest grove
#

its weird that the type is being returned properly here but its able to be passed into handleStylesConfigChanged with the incorrect type:

prime lightBOT
#
kinghat#7157

Preview:ts ... const test1 = handleStylesConfigChanged({}, attr1) ...

vocal edge
#

the type returned is non nullable, so it is of correct typings.

earnest grove
#

shouldnt the second parameter only take a Style type?
const handleStylesConfigChanged = (eventConfig: {}, config: Styles) => {}

vocal edge
#

Ah ! Indeed. But in your playground, types Style / Attributes / Options are actually equal. In typescript, types/interfaces are not strong.

#

Add a specific property to your Styles type and watch what happens.

earnest grove
#

"are not strong"?

vocal edge
#

Types are contracts.

type Duck = {
quack:()=>void
}

Here you are telling typescript "everything that can quack is a duck"

#

by doing

type Attributes = {}
type Styles = {}
type Options = {}

You are telling typescript "any non-null and non-undefined object is an attribute, a style, and an option at the same time"

earnest grove
#

each type isnt a unique contract?

vocal edge
#

Nope ! types are basically aliases . You are giving a name to a construct.

#

In your example, you can do

const attributes:Attributes = new Date():
const styles:Styles = new Error();
const options:Options = styles

and it works fine ! they are actually different aliases for {}

earnest grove
#

in my app where im applying this the contracts are:

export interface Attributes {
  label?: string;
  icon?: string;
  isFadingIndicator?: boolean;
  minWidth?: boolean;
  isMinWidthIndicator?: boolean;
  stacked?: boolean;
}
export interface Styles
  extends Partial<DefaultConfigColorProperties>,
    Partial<DefaultConfigNonColorProperties> {
  [x: string]: string;
}

export interface Options {
  defaultTabIndex?: number;
  tabsAtTop?: boolean;
}
#

because {} is valid for interface Styles as shown above.

vocal edge
#

Yus ! You can check that with this

type Test = {} extends Styles? true: false;
const test:Test = true
earnest grove
#

thanks for your help @vocal edge! im going to mark as resolved. i might post back so hopefully you dont mind the ping.

#

!resolved

earnest grove
#

if i wanted to preemptively get the global config based on the propertyKey and store it in globalPropertyConfig, why doesnt the type carry through when narrowing with propertyKey === "styles"? if i just use getGlobalConfigProperty(propertyKey) directly it carries fine as shown in the bottom two examples:

prime lightBOT
#
kinghat#7157

Preview:ts ... const changedConfig = propertyKey === "attributes" ? handleStylesConfigChanged( {}, globalPropertyConfig ) : {} const changedConfig1 = propertyKey === "styles" ? handleStylesConfigChanged( {}, globalPropertyConfig ) : {} ...

earnest grove
#

im also confused about the NonNullable return type here. why is it showing NonNullable at all and also carrying the undefined further?:

const globalPropertyConfig: NonNullable<Attributes | Styles | Options | undefined>